refactor(indeedhub): delete orchestrator special-cases; use generic path (#20 phase 3)
The fresh-create path was blocked by hardcoded indeedhub orchestrator logic
that predated and conflicted with the manifest migration:
- ensure_running routed app_id=="indeedhub" → reconcile_indeedhub_stack, which
REFUSED to create the frontend from its manifest (returned Left("stack-managed")).
- run_pre_start_hooks("indeedhub") → start_indeedhub_backends →
wait_for_indeedhub_dependencies_ready(120) — a DNS gate with a chicken-and-egg
bug (required the frontend's own alias present before the frontend could be
created), which failed install_fresh with "dependencies were not ready within
120s" and left the frontend down (caught live on .228).
Delete all of it (−382 lines): reconcile_indeedhub_stack, start_indeedhub_backends,
wait_for_indeedhub_dependencies_ready, indeedhub_api_dependency_dns_ready,
indeedhub_required_aliases_present, repair_indeedhub_network_aliases,
indeedhub_alias_present, patch_indeedhub_nostr_provider, and the INDEEDHUB_*
consts. The manifests now carry everything these did: network_aliases (short
hostnames), generated_secrets, dependencies, and the post_install nginx hook. So
"indeedhub" + every member flows through the generic install_fresh/reconcile path
— the frontend fresh-creates normally and runs its hook.
(crash_recovery.rs's frontend-after-deps ordering guard is kept — it's beneficial
startup ordering, not a blocker.) cargo check + release build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
84031e6209
commit
b73084dbb0
@ -50,15 +50,6 @@ use crate::update::host_sudo;
|
||||
/// so the rule is visible in one place and unit-testable.
|
||||
const UI_APP_IDS: &[&str] = &["bitcoin-ui", "electrs-ui", "lnd-ui"];
|
||||
const ARCHIVAL_BITCOIN_DISK_GB: u64 = 1000;
|
||||
const INDEEDHUB_BACKEND_CONTAINERS: &[&str] = &[
|
||||
"indeedhub-postgres",
|
||||
"indeedhub-redis",
|
||||
"indeedhub-minio",
|
||||
"indeedhub-relay",
|
||||
"indeedhub-api",
|
||||
"indeedhub-ffmpeg",
|
||||
];
|
||||
const INDEEDHUB_FRONTEND_READY_TIMEOUT_SECS: u64 = 90;
|
||||
|
||||
fn is_required_baseline_app(app_id: &str) -> bool {
|
||||
matches!(
|
||||
@ -765,102 +756,6 @@ async fn restart_container_scoped_if_pasta(
|
||||
}
|
||||
}
|
||||
|
||||
async fn patch_indeedhub_nostr_provider() {
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args([
|
||||
"exec",
|
||||
"indeedhub",
|
||||
"sed",
|
||||
"-i",
|
||||
"/X-Frame-Options/d",
|
||||
"/etc/nginx/conf.d/default.conf",
|
||||
])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
let provider_src = "/opt/archipelago/web-ui/nostr-provider.js";
|
||||
if tokio::fs::metadata(provider_src).await.is_ok() {
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args([
|
||||
"cp",
|
||||
provider_src,
|
||||
"indeedhub:/usr/share/nginx/html/nostr-provider.js",
|
||||
])
|
||||
.output()
|
||||
.await;
|
||||
}
|
||||
|
||||
let check = tokio::process::Command::new("podman")
|
||||
.args([
|
||||
"exec",
|
||||
"indeedhub",
|
||||
"grep",
|
||||
"-q",
|
||||
"nostr-provider",
|
||||
"/etc/nginx/conf.d/default.conf",
|
||||
])
|
||||
.output()
|
||||
.await;
|
||||
let already_patched = check.map(|o| o.status.success()).unwrap_or(false);
|
||||
|
||||
if !already_patched {
|
||||
let cat_out = tokio::process::Command::new("podman")
|
||||
.args(["exec", "indeedhub", "cat", "/etc/nginx/conf.d/default.conf"])
|
||||
.output()
|
||||
.await;
|
||||
if let Ok(out) = cat_out {
|
||||
if out.status.success() {
|
||||
let conf = String::from_utf8_lossy(&out.stdout).to_string();
|
||||
let conf = conf.replace(
|
||||
"location = /sw.js {",
|
||||
"location = /nostr-provider.js {\n\
|
||||
add_header Cache-Control \"no-cache, no-store, must-revalidate\";\n\
|
||||
expires off;\n\
|
||||
}\n\n\
|
||||
location = /sw.js {",
|
||||
);
|
||||
let conf = if conf.contains("try_files") && !conf.contains("sub_filter") {
|
||||
conf.replacen(
|
||||
"try_files $uri $uri/ /index.html;",
|
||||
"try_files $uri $uri/ /index.html;\n\
|
||||
sub_filter_once on;\n\
|
||||
sub_filter '</head>' '<script src=\"/nostr-provider.js\"></script></head>';",
|
||||
1,
|
||||
)
|
||||
} else {
|
||||
conf
|
||||
};
|
||||
|
||||
let tmp_path = "/tmp/indeedhub-nginx-patch.conf";
|
||||
if tokio::fs::write(tmp_path, &conf).await.is_ok() {
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args(["cp", tmp_path, "indeedhub:/etc/nginx/conf.d/default.conf"])
|
||||
.output()
|
||||
.await;
|
||||
let _ = tokio::fs::remove_file(tmp_path).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args([
|
||||
"exec",
|
||||
"indeedhub",
|
||||
"sed",
|
||||
"-i",
|
||||
"s|proxy_set_header X-Forwarded-Prefix /api;|proxy_set_header X-Forwarded-Prefix $http_x_forwarded_prefix/api;|",
|
||||
"/etc/nginx/conf.d/default.conf",
|
||||
])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args(["exec", "indeedhub", "nginx", "-s", "reload"])
|
||||
.output()
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Outcome of `reconcile_all` for a single app.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ReconcileAction {
|
||||
@ -1374,13 +1269,12 @@ impl ProdContainerOrchestrator {
|
||||
mode: ReconcileMode,
|
||||
) -> Result<ReconcileAction> {
|
||||
let app_id = lm.manifest.app.id.clone();
|
||||
if app_id == "indeedhub" {
|
||||
// IndeedHub is a multi-container stack installed by the package
|
||||
// stack path. Boot reconcile must not fresh-install the catalog
|
||||
// manifest, but it does need to start/repair an already-installed
|
||||
// stack and reapply the frontend's Nostr provider patch after boot.
|
||||
return self.reconcile_indeedhub_stack(mode).await;
|
||||
}
|
||||
// IndeedHub used to be a hardcoded orchestrator special-case
|
||||
// (reconcile_indeedhub_stack + a dependency-DNS gate) that refused to
|
||||
// create the frontend from its manifest. It is now fully manifest-driven
|
||||
// (apps/indeedhub-* + apps/indeedhub): network_aliases, generated_secrets,
|
||||
// dependencies, and the post_install nginx hook live in the manifests, so
|
||||
// every member — frontend included — flows through the generic path here.
|
||||
let lock = self.app_lock(&app_id).await;
|
||||
let _guard = lock.lock().await;
|
||||
|
||||
@ -2321,10 +2215,6 @@ impl ProdContainerOrchestrator {
|
||||
self.ensure_btcpay_stack_dirs().await?;
|
||||
Ok(Some(HookOutcome::Unchanged))
|
||||
}
|
||||
"indeedhub" => {
|
||||
self.start_indeedhub_backends().await?;
|
||||
Ok(Some(HookOutcome::Unchanged))
|
||||
}
|
||||
"grafana" => {
|
||||
self.cleanup_stale_grafana_port().await;
|
||||
Ok(Some(HookOutcome::Unchanged))
|
||||
@ -2398,272 +2288,6 @@ impl ProdContainerOrchestrator {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_indeedhub_backends(&self) -> Result<()> {
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args(["network", "create", "indeedhub-net"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
for name in INDEEDHUB_BACKEND_CONTAINERS {
|
||||
let status = match self.runtime.get_container_status(name).await {
|
||||
Ok(status) => status,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if !matches!(status.state, ContainerState::Running) {
|
||||
if let Err(err) = podman_user_scope(&["start", name]).await {
|
||||
tracing::warn!(
|
||||
container = %name,
|
||||
error = %err,
|
||||
"IndeedHub scoped backend start failed; falling back to runtime start"
|
||||
);
|
||||
self.runtime
|
||||
.start_container(name)
|
||||
.await
|
||||
.with_context(|| format!("start IndeedHub backend {name}"))?;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
}
|
||||
}
|
||||
self.repair_indeedhub_network_aliases().await;
|
||||
self.wait_for_indeedhub_dependencies_ready(120).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn reconcile_indeedhub_stack(&self, mode: ReconcileMode) -> Result<ReconcileAction> {
|
||||
let frontend_status = match self.runtime.get_container_status("indeedhub").await {
|
||||
Ok(status) => status,
|
||||
Err(_) => {
|
||||
if mode == ReconcileMode::ExistingOnly {
|
||||
return Ok(ReconcileAction::Left("absent".to_string()));
|
||||
}
|
||||
// Fresh stack creation is owned by package::stacks so we do not
|
||||
// create a single broken frontend container from the manifest.
|
||||
return Ok(ReconcileAction::Left("stack-managed".to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
self.start_indeedhub_backends().await?;
|
||||
|
||||
let mut started = false;
|
||||
match frontend_status.state {
|
||||
ContainerState::Running => {}
|
||||
ContainerState::Stopped
|
||||
| ContainerState::Exited
|
||||
| ContainerState::Created
|
||||
| ContainerState::Stopping => {
|
||||
if let Err(err) = podman_user_scope(&["start", "indeedhub"]).await {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"IndeedHub scoped frontend start failed; falling back to runtime start"
|
||||
);
|
||||
self.runtime
|
||||
.start_container("indeedhub")
|
||||
.await
|
||||
.context("start IndeedHub frontend during reconcile")?;
|
||||
}
|
||||
started = true;
|
||||
}
|
||||
ContainerState::Paused => return Ok(ReconcileAction::Left("paused".to_string())),
|
||||
ContainerState::Unknown(s) => return Ok(ReconcileAction::Left(s)),
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
self.repair_indeedhub_network_aliases().await;
|
||||
patch_indeedhub_nostr_provider().await;
|
||||
|
||||
let frontend_stable = wait_for_container_stable_running(
|
||||
self.runtime.as_ref(),
|
||||
"indeedhub",
|
||||
5,
|
||||
INDEEDHUB_FRONTEND_READY_TIMEOUT_SECS,
|
||||
)
|
||||
.await;
|
||||
if frontend_stable.is_err() || !wait_for_host_port(7778, 10).await {
|
||||
tracing::warn!(
|
||||
error = ?frontend_stable.err(),
|
||||
"IndeedHub frontend did not stay reachable after reconcile; restarting"
|
||||
);
|
||||
let _ = self.runtime.stop_container("indeedhub").await;
|
||||
if let Err(err) = podman_user_scope(&["start", "indeedhub"]).await {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"IndeedHub scoped frontend restart failed; falling back to runtime start"
|
||||
);
|
||||
self.runtime
|
||||
.start_container("indeedhub")
|
||||
.await
|
||||
.context("restart IndeedHub frontend after failed readiness")?;
|
||||
}
|
||||
started = true;
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
patch_indeedhub_nostr_provider().await;
|
||||
wait_for_container_stable_running(
|
||||
self.runtime.as_ref(),
|
||||
"indeedhub",
|
||||
5,
|
||||
INDEEDHUB_FRONTEND_READY_TIMEOUT_SECS,
|
||||
)
|
||||
.await
|
||||
.context("IndeedHub frontend did not remain running after restart")?;
|
||||
if !wait_for_host_port(7778, 30).await {
|
||||
return Err(anyhow::anyhow!(
|
||||
"IndeedHub frontend did not expose host port 7778 after restart"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if started {
|
||||
Ok(ReconcileAction::Started)
|
||||
} else {
|
||||
Ok(ReconcileAction::NoOp)
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_indeedhub_dependencies_ready(&self, timeout_secs: u64) -> Result<()> {
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(timeout_secs);
|
||||
let mut last = String::from("not checked");
|
||||
loop {
|
||||
let mut all_running = true;
|
||||
for name in INDEEDHUB_BACKEND_CONTAINERS {
|
||||
match self.runtime.get_container_status(name).await {
|
||||
Ok(status) if matches!(status.state, ContainerState::Running) => {}
|
||||
Ok(status) => {
|
||||
all_running = false;
|
||||
last = format!("{name} state {:?}", status.state);
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
all_running = false;
|
||||
last = format!("{name} status error: {err}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if all_running && self.indeedhub_api_dependency_dns_ready().await {
|
||||
return Ok(());
|
||||
}
|
||||
if all_running {
|
||||
last = "indeedhub-api dependency DNS not ready".to_string();
|
||||
}
|
||||
|
||||
if std::time::Instant::now() >= deadline {
|
||||
return Err(anyhow::anyhow!(
|
||||
"IndeedHub dependencies were not ready within {}s ({})",
|
||||
timeout_secs,
|
||||
last
|
||||
));
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn indeedhub_api_dependency_dns_ready(&self) -> bool {
|
||||
let aliases_ready = self.indeedhub_required_aliases_present().await;
|
||||
if cfg!(test) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for host in ["postgres", "redis", "minio", "relay"] {
|
||||
let Ok(Ok(output)) = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
tokio::process::Command::new("podman")
|
||||
.args(["exec", "indeedhub-api", "getent", "hosts", host])
|
||||
.output(),
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return aliases_ready;
|
||||
};
|
||||
if !output.status.success() {
|
||||
return aliases_ready;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
async fn indeedhub_required_aliases_present(&self) -> bool {
|
||||
for (container, alias) in [
|
||||
("indeedhub-postgres", "postgres"),
|
||||
("indeedhub-redis", "redis"),
|
||||
("indeedhub-minio", "minio"),
|
||||
("indeedhub-relay", "relay"),
|
||||
("indeedhub-api", "api"),
|
||||
("indeedhub", "indeedhub"),
|
||||
] {
|
||||
if !self.indeedhub_alias_present(container, alias).await {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
async fn repair_indeedhub_network_aliases(&self) {
|
||||
for (container, alias) in [
|
||||
("indeedhub-postgres", "postgres"),
|
||||
("indeedhub-redis", "redis"),
|
||||
("indeedhub-minio", "minio"),
|
||||
("indeedhub-relay", "relay"),
|
||||
("indeedhub-api", "api"),
|
||||
("indeedhub", "indeedhub"),
|
||||
] {
|
||||
let exists = tokio::process::Command::new("podman")
|
||||
.args(["container", "exists", container])
|
||||
.status()
|
||||
.await
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false);
|
||||
if !exists {
|
||||
continue;
|
||||
}
|
||||
if self.indeedhub_alias_present(container, alias).await {
|
||||
continue;
|
||||
}
|
||||
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args(["network", "disconnect", "-f", "indeedhub-net", container])
|
||||
.output()
|
||||
.await;
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args([
|
||||
"network",
|
||||
"connect",
|
||||
"--alias",
|
||||
alias,
|
||||
"indeedhub-net",
|
||||
container,
|
||||
])
|
||||
.output()
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn indeedhub_alias_present(&self, container: &str, alias: &str) -> bool {
|
||||
let output = match tokio::process::Command::new("podman")
|
||||
.args([
|
||||
"inspect",
|
||||
container,
|
||||
"--format",
|
||||
"{{json .NetworkSettings.Networks}}",
|
||||
])
|
||||
.output()
|
||||
.await
|
||||
{
|
||||
Ok(output) if output.status.success() => output,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let Ok(networks) = serde_json::from_slice::<serde_json::Value>(&output.stdout) else {
|
||||
return false;
|
||||
};
|
||||
networks
|
||||
.get("indeedhub-net")
|
||||
.and_then(|network| network.get("Aliases"))
|
||||
.and_then(|aliases| aliases.as_array())
|
||||
.map(|aliases| aliases.iter().any(|value| value.as_str() == Some(alias)))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn cleanup_stale_grafana_port(&self) {
|
||||
let _ = tokio::process::Command::new("pkill")
|
||||
.args(["-f", "pasta.*3001"])
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user