fix(orchestrator): generalize launch-port fallback + archival-bitcoin dependency gating

Master-plan backlog §10b/§10c: replace two per-app-hardcoded lookups with
generic, manifest-driven behavior so future apps are covered automatically
instead of needing a code edit.

- extract_lan_address (docker_packages.rs) now skips container-side ports
  that are known non-HTTP (SSH, FTP, common DB ports) instead of blindly
  taking podman's first-listed port. Fixes the whole class of bug the gitea
  SSH-before-web static override was a one-off patch for.
- requires_unpruned_bitcoin (dependencies.rs) now checks the app's own
  manifest for a `bitcoin:archival` dependency declaration first, falling
  back to the old hardcoded id list. electrumx and mempool manifests now
  declare it explicitly as the proof case.

869/869 Rust tests green, catalog drift clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-07-01 03:59:00 -04:00
co-authored by Claude Sonnet 5
parent 46dae75a0f
commit 306b6356ee
5 changed files with 164 additions and 9 deletions
@@ -1,6 +1,8 @@
use super::config::get_containers_for_app;
use super::runtime::manifest_apps_dirs;
use crate::data_model::{PackageDataEntry, PackageState};
use anyhow::{Context, Result};
use archipelago_container::{AppManifest, Dependency};
use std::collections::HashMap;
use tracing::info;
@@ -11,7 +13,38 @@ const BITCOIN_NAMES: &[&str] = &["bitcoin-knots", "bitcoin-core", "bitcoin"];
const ELECTRUM_NAMES: &[&str] = &["electrumx", "mempool-electrs", "electrs"];
const ARCHIVAL_BITCOIN_DISK_GB: u64 = 1000;
/// The manifest string dependency that declares "needs an archival
/// (unpruned + txindex) Bitcoin node" — see `manifest_declares_archival_bitcoin`.
const ARCHIVAL_BITCOIN_DEPENDENCY: &str = "bitcoin:archival";
/// Whether `package_id`'s own on-disk manifest declares
/// `dependencies: [bitcoin:archival]`. Manifest-driven alternative to the
/// hardcoded id list below — a new app just declares the dependency instead
/// of needing a code change here.
fn manifest_declares_archival_bitcoin(package_id: &str) -> bool {
for apps_dir in manifest_apps_dirs() {
let path = apps_dir.join(package_id).join("manifest.yml");
let Ok(contents) = std::fs::read_to_string(&path) else {
continue;
};
let Ok(manifest) = AppManifest::parse(&contents) else {
continue;
};
return dependency_list_declares_archival_bitcoin(&manifest.app.dependencies);
}
false
}
fn dependency_list_declares_archival_bitcoin(deps: &[Dependency]) -> bool {
deps.iter()
.any(|dep| matches!(dep, Dependency::Simple(s) if s == ARCHIVAL_BITCOIN_DEPENDENCY))
}
fn requires_unpruned_bitcoin(package_id: &str) -> bool {
if manifest_declares_archival_bitcoin(package_id) {
return true;
}
// Fallback for apps not yet migrated to the manifest declaration above.
matches!(
package_id,
"electrumx" | "mempool-electrs" | "electrs" | "mempool" | "mempool-web"
@@ -473,7 +506,11 @@ pub(super) fn configure_fedimint_lnd(
#[cfg(test)]
mod tests {
use super::{order_present_containers, requires_unpruned_bitcoin, startup_order};
use super::{
dependency_list_declares_archival_bitcoin, manifest_declares_archival_bitcoin,
order_present_containers, requires_unpruned_bitcoin, startup_order,
};
use archipelago_container::Dependency;
#[test]
fn order_present_containers_never_injects_phantom_stack_members() {
@@ -547,4 +584,44 @@ mod tests {
assert!(!requires_unpruned_bitcoin(package_id), "{package_id}");
}
}
#[test]
fn dependency_matcher_finds_the_archival_marker_among_other_deps() {
let deps = vec![
Dependency::App {
app_id: "bitcoin-knots".to_string(),
version: Some(">=26.0".to_string()),
},
Dependency::Storage {
storage: "50Gi".to_string(),
},
Dependency::Simple("bitcoin:archival".to_string()),
];
assert!(dependency_list_declares_archival_bitcoin(&deps));
}
#[test]
fn dependency_matcher_false_when_marker_absent() {
let deps = vec![Dependency::App {
app_id: "bitcoin-knots".to_string(),
version: Some(">=26.0".to_string()),
}];
assert!(!dependency_list_declares_archival_bitcoin(&deps));
assert!(!dependency_list_declares_archival_bitcoin(&[]));
}
#[test]
fn manifest_declared_archival_bitcoin_covers_a_new_app_without_a_code_change() {
// electrumx and mempool declare `dependencies: [..., bitcoin:archival]`
// on disk (apps/electrumx/manifest.yml, apps/mempool/manifest.yml) —
// this is the manifest-driven path working end-to-end, not the
// hardcoded id list. A future app only needs this manifest line, no
// edit to `requires_unpruned_bitcoin`.
assert!(manifest_declares_archival_bitcoin("electrumx"));
assert!(manifest_declares_archival_bitcoin("mempool"));
// An app whose manifest exists but never declares the marker.
assert!(!manifest_declares_archival_bitcoin("bitcoin-knots"));
// An id with no manifest on disk at all.
assert!(!manifest_declares_archival_bitcoin("does-not-exist"));
}
}
@@ -1603,7 +1603,7 @@ fn manifest_host_ports(container_name: &str) -> Vec<u16> {
Vec::new()
}
fn manifest_apps_dirs() -> Vec<std::path::PathBuf> {
pub(super) fn manifest_apps_dirs() -> Vec<std::path::PathBuf> {
let mut dirs = Vec::new();
if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
dirs.push(Path::new(&manifest_dir).join("../../apps"));