Add headless Angor services and shared-index install guard
Demo images / Build & push demo images (push) Failing after 43s
Demo images / Build & push demo images (push) Failing after 43s
This commit is contained in:
@@ -22,6 +22,14 @@ const ARCHIVAL_BITCOIN_DEPENDENCY: &str = "bitcoin:archival";
|
||||
/// 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 {
|
||||
// Registry-only apps need the same guard as OTA-bundled manifests. Honor
|
||||
// the verified catalog's effective manifest before the disk fallback.
|
||||
if let Some((_, value)) = crate::container::app_catalog::catalog_manifest_values()
|
||||
.into_iter().find(|(id, _)| id == package_id) {
|
||||
if let Some(manifest) = crate::container::app_catalog::catalog_manifest_overlay(package_id, value) {
|
||||
return dependency_list_declares_archival_bitcoin(&manifest.app.dependencies);
|
||||
}
|
||||
}
|
||||
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 {
|
||||
@@ -1055,6 +1063,9 @@ mod tests {
|
||||
// edit to `requires_unpruned_bitcoin`.
|
||||
assert!(manifest_declares_archival_bitcoin("electrumx"));
|
||||
assert!(manifest_declares_archival_bitcoin("mempool"));
|
||||
let angor = archipelago_container::AppManifest::parse(include_str!(concat!(env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../apps/angor-indexer/manifest.yml"))).unwrap();
|
||||
assert!(dependency_list_declares_archival_bitcoin(&angor.app.dependencies));
|
||||
// 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.
|
||||
|
||||
@@ -573,6 +573,9 @@ impl RpcHandler {
|
||||
"message": format!("Package {} installed and started", package_id)
|
||||
}));
|
||||
}
|
||||
Err(e) if e.downcast_ref::<crate::container::prod_orchestrator::InstallPrerequisiteError>().is_some() => {
|
||||
return Err(super::dependencies::DependencyGateError(e.to_string()).into());
|
||||
}
|
||||
Err(e) if is_unknown_app_id_error(&e) => {
|
||||
info!(
|
||||
"Install {}: orchestrator has no manifest mapping yet, falling back to legacy installer",
|
||||
|
||||
@@ -36,6 +36,11 @@ use std::sync::Arc;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
|
||||
/// Refusal before installation has created state or changed any dependency.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("{0}")]
|
||||
pub struct InstallPrerequisiteError(pub String);
|
||||
|
||||
use crate::config::{Config, ContainerRuntime as ConfigContainerRuntime};
|
||||
use crate::container::bitcoin_ui;
|
||||
use crate::container::quadlet;
|
||||
@@ -3904,7 +3909,7 @@ impl ProdContainerOrchestrator {
|
||||
let exists = tokio::process::Command::new("podman")
|
||||
.args(["container", "exists", name]).status().await?;
|
||||
if exists.code() != Some(1) {
|
||||
anyhow::bail!("cannot verify existing container before network migration backup");
|
||||
anyhow::bail!("cannot verify existing container before runtime migration backup");
|
||||
}
|
||||
false
|
||||
};
|
||||
@@ -3922,7 +3927,7 @@ impl ProdContainerOrchestrator {
|
||||
}
|
||||
match crate::container::migration_backup::snapshot(manifest, &self.data_dir, previous_unit.as_deref()).await {
|
||||
Ok(archive) => {
|
||||
tracing::info!(container = %name, backup = %archive.display(), "Persistent state saved before network migration");
|
||||
tracing::info!(container = %name, backup = %archive.display(), "Persistent state saved before runtime migration");
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -4551,6 +4556,28 @@ impl ContainerOrchestrator for ProdContainerOrchestrator {
|
||||
}
|
||||
|
||||
async fn install(&self, app_id: &str) -> Result<String> {
|
||||
let lm = self.loaded(app_id).await?;
|
||||
// Optional shared-service preconditions are checked before recording
|
||||
// installation or creating anything. A headless adapter must not claim
|
||||
// successful installation against a missing indexing stack.
|
||||
if let Some(required) = lm.manifest.app.extensions.get("install_prerequisites")
|
||||
.and_then(|value| value.as_sequence()) {
|
||||
let present = self.runtime.list_containers().await
|
||||
.context("check installed prerequisite services")?;
|
||||
for id in required.iter().filter_map(|value| value.as_str()) {
|
||||
let dependency = self.loaded(id).await.map_err(|_| InstallPrerequisiteError(
|
||||
format!("Required app {id} is unavailable. Refresh the app catalog before installing {}.",
|
||||
lm.manifest.app.name)))?;
|
||||
let name = compute_container_name(&dependency.manifest);
|
||||
if !present.iter().any(|container| container.name.trim_start_matches('/') == name) {
|
||||
let owner = crate::app_ops::owning_package(id);
|
||||
let title = self.loaded(owner).await.map(|app| app.manifest.app.name)
|
||||
.unwrap_or(dependency.manifest.app.name);
|
||||
return Err(InstallPrerequisiteError(format!(
|
||||
"Install {title} first, then install {}.", lm.manifest.app.name)).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut state = self.state.write().await;
|
||||
state.disabled.remove(app_id);
|
||||
@@ -4577,7 +4604,6 @@ impl ContainerOrchestrator for ProdContainerOrchestrator {
|
||||
// health verification (the .228 "running but unreachable" failure
|
||||
// mode). Routing every install through here means the orchestrator
|
||||
// is the one source of truth for what "installed" means.
|
||||
let lm = self.loaded(app_id).await?;
|
||||
let name = compute_container_name(&lm.manifest);
|
||||
// ensure_running takes the per-app lock itself; release the install
|
||||
// path lock first if we hold one (we don't — install is the entry
|
||||
@@ -5746,6 +5772,26 @@ app:
|
||||
orch
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_install_prerequisite_refuses_without_inventory_or_container_mutation() {
|
||||
let rt = Arc::new(MockRuntime::default());
|
||||
let orch = orch_with(rt.clone()).await;
|
||||
let mut app = pull_manifest("indexer-adapter", "docker.io/library/alpine:3.20");
|
||||
app.app.extensions.insert("install_prerequisites".into(),
|
||||
serde_yaml::to_value(vec!["shared-index"]).unwrap());
|
||||
orch.insert_manifest_for_test(app, PathBuf::from("/tmp")).await;
|
||||
orch.insert_manifest_for_test(pull_manifest("shared-index", "index:1"), PathBuf::from("/tmp")).await;
|
||||
let error = orch.install("indexer-adapter").await.unwrap_err();
|
||||
assert!(error.downcast_ref::<InstallPrerequisiteError>().is_some());
|
||||
assert!(!crate::crash_recovery::load_installed_apps(&orch.data_dir).await.contains("indexer-adapter"));
|
||||
assert_eq!(rt.calls(), vec!["list_containers"]);
|
||||
// An installed prerequisite satisfies the guard; it is never recreated
|
||||
// or reconfigured as part of installing this adapter.
|
||||
rt.set_state("shared-index", ContainerState::Running);
|
||||
orch.install("indexer-adapter").await.unwrap();
|
||||
assert!(!rt.calls().iter().any(|c| c.starts_with("create_container:shared-index")));
|
||||
}
|
||||
|
||||
fn pull_manifest_with_dynamic_env(id: &str, image: &str) -> AppManifest {
|
||||
let yaml = format!(
|
||||
"app:\n id: {id}\n name: {id}\n version: 1.0.0\n container:\n image: {image}\n derived_env:\n - key: FM_API_URL\n template: \"ws://{{{{HOST_MDNS}}}}:8174\"\n secret_env:\n - key: FM_BITCOIND_PASSWORD\n secret_file: bitcoin-rpc-password\n environment:\n - STATIC=1\n"
|
||||
|
||||
@@ -6,7 +6,43 @@
|
||||
//! no listener, so allowing them is inert.
|
||||
|
||||
pub const APP_LAUNCH_PORTS: &[u16] = &[
|
||||
2283, 2342, 3000, 3001, 3002, 4080, 5180, 7778, 8080, 8081, 8082, 8083, 8084, 8085, 8087, 8090,
|
||||
8096, 8123, 8175, 8176, 8187, 8240, 8334, 8336, 8337, 8888, 8999, 9000, 9100, 10380, 11434,
|
||||
18081, 18083, 18091, 23000, 32838, 50002,
|
||||
2283,
|
||||
2342,
|
||||
3000,
|
||||
3001,
|
||||
3002,
|
||||
4080,
|
||||
5180,
|
||||
7778,
|
||||
8080,
|
||||
8081,
|
||||
8082,
|
||||
8083,
|
||||
8084,
|
||||
8085,
|
||||
8087,
|
||||
8090,
|
||||
8091,
|
||||
8096,
|
||||
8123,
|
||||
8175,
|
||||
8176,
|
||||
8187,
|
||||
8240,
|
||||
8334,
|
||||
8336,
|
||||
8337,
|
||||
8888,
|
||||
8998,
|
||||
8999,
|
||||
9000,
|
||||
9100,
|
||||
10380,
|
||||
11434,
|
||||
18081,
|
||||
18083,
|
||||
18091,
|
||||
23000,
|
||||
32838,
|
||||
50002,
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user