release(v1.7.31-alpha): idempotent IndeedHub install + auto-merge default mirrors/registries + 3rd OVH update mirror
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Has been cancelled
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Has been cancelled
- Backend: install.rs registry reachability probe now strips the `host[:port]/namespace` suffix before appending `/v2/` (the Docker V2 API lives at the host root, not under the namespace) and accepts HTTP 405 in addition to 200/401 as "registry daemon alive". This fixes false "unreachable" reports on the Test button for Gitea and other registries that protect their /v2/ endpoint. - Backend: stacks.rs install_indeedhub_stack now force-removes any leftover indeedhub-* containers and indeedhub-net before creating the stack. A partial install (or the old first-boot stub racing the installer) used to leave containers around that blocked re-install with "name already in use". Re-running the App Store install now self-heals. - Backend: registry.rs load_registries auto-merges any default registry URLs missing from the saved config (appended with priority max+10+i, persisted). Lets new default mirrors (e.g. Server 3 OVH) roll out to existing nodes without manual config edits. Explicit removals still stick — URLs absent from disk AND absent from defaults stay gone. - Backend: update.rs adds DEFAULT_TERTIARY_MIRROR_URL at http://146.59.87.168:3000/ (Server 3 OVH) to default_mirrors, with the same auto-merge-on-load behavior as registries. Test updated for 3-mirror default (.160, tx1138, .168). - Scripts: dropped the first-boot IndeedHub stub (~38 lines in first-boot-containers.sh §8b). It predated the proper stack installer, raced it, and was the main source of the name-conflict mess the stacks.rs cleanup above now also guards against. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f9b44f5e2e
commit
fdaa5646b2
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.7.30-alpha"
|
version = "1.7.31-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"archipelago-container",
|
"archipelago-container",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.7.30-alpha"
|
version = "1.7.31-alpha"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||||
authors = ["Archipelago Team"]
|
authors = ["Archipelago Team"]
|
||||||
|
|||||||
@ -1589,10 +1589,16 @@ server {
|
|||||||
.and_then(|v| v.as_bool())
|
.and_then(|v| v.as_bool())
|
||||||
.unwrap_or(true);
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
// Registries are configured as `host[:port]/namespace` (for
|
||||||
|
// example `23.182.128.160:3000/lfg2025`), but the Docker V2
|
||||||
|
// registry API lives at `/v2/` on the ROOT of the host — NOT
|
||||||
|
// under the namespace. Strip the namespace before appending
|
||||||
|
// `/v2/` so the reachability probe hits the correct URL.
|
||||||
|
let host = url.split('/').next().unwrap_or(url);
|
||||||
let test_url = if tls_verify {
|
let test_url = if tls_verify {
|
||||||
format!("https://{}/v2/", url)
|
format!("https://{}/v2/", host)
|
||||||
} else {
|
} else {
|
||||||
format!("http://{}/v2/", url)
|
format!("http://{}/v2/", host)
|
||||||
};
|
};
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
@ -1604,8 +1610,11 @@ server {
|
|||||||
match client.get(&test_url).send().await {
|
match client.get(&test_url).send().await {
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
let status = resp.status().as_u16();
|
let status = resp.status().as_u16();
|
||||||
// 200 = open registry, 401 = auth required (both mean it exists)
|
// Accept any "the server responded to HTTP" code as
|
||||||
let reachable = status == 200 || status == 401;
|
// reachable — 200 (open registry), 401 (auth required),
|
||||||
|
// or 405 (server rejects GET but the endpoint exists)
|
||||||
|
// all mean the registry daemon is alive on the host.
|
||||||
|
let reachable = status == 200 || status == 401 || status == 405;
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"url": url,
|
"url": url,
|
||||||
"reachable": reachable,
|
"reachable": reachable,
|
||||||
|
|||||||
@ -937,6 +937,31 @@ impl RpcHandler {
|
|||||||
.with_context(|| format!("Failed to pull IndeedHub image: {}", img))?;
|
.with_context(|| format!("Failed to pull IndeedHub image: {}", img))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove any leftover containers from a previous partial install (or
|
||||||
|
// from the first-boot frontend stub that used to race the installer).
|
||||||
|
// Without this, `podman run --name indeedhub` fails on name conflict
|
||||||
|
// and the whole stack install errors out — leaving a half-broken node.
|
||||||
|
for name in [
|
||||||
|
"indeedhub",
|
||||||
|
"indeedhub-postgres",
|
||||||
|
"indeedhub-redis",
|
||||||
|
"indeedhub-minio",
|
||||||
|
"indeedhub-relay",
|
||||||
|
"indeedhub-api",
|
||||||
|
"indeedhub-ffmpeg",
|
||||||
|
"indeedhub-build_api_1",
|
||||||
|
"indeedhub-build_ffmpeg-worker_1",
|
||||||
|
] {
|
||||||
|
let _ = tokio::process::Command::new("podman")
|
||||||
|
.args(["rm", "-f", name])
|
||||||
|
.status()
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
let _ = tokio::process::Command::new("podman")
|
||||||
|
.args(["network", "rm", "-f", "indeedhub-net"])
|
||||||
|
.status()
|
||||||
|
.await;
|
||||||
|
|
||||||
// Create indeedhub-net
|
// Create indeedhub-net
|
||||||
let _ = tokio::process::Command::new("podman")
|
let _ = tokio::process::Command::new("podman")
|
||||||
.args(["network", "create", "indeedhub-net"])
|
.args(["network", "create", "indeedhub-net"])
|
||||||
|
|||||||
@ -97,7 +97,12 @@ fn extract_image_name(image: &str) -> &str {
|
|||||||
image.rsplit('/').next().unwrap_or(image)
|
image.rsplit('/').next().unwrap_or(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load registry config from disk.
|
/// Load registry config from disk, merging in any default registries
|
||||||
|
/// that the operator hasn't explicitly removed. This lets us roll out
|
||||||
|
/// new default mirrors (e.g. a new Server 3) to existing nodes without
|
||||||
|
/// them having to edit their saved config. Explicit removals stick —
|
||||||
|
/// if the URL is absent from disk AND absent from current defaults, it
|
||||||
|
/// stays gone.
|
||||||
pub async fn load_registries(data_dir: &Path) -> Result<RegistryConfig> {
|
pub async fn load_registries(data_dir: &Path) -> Result<RegistryConfig> {
|
||||||
let path = data_dir.join(REGISTRY_FILE);
|
let path = data_dir.join(REGISTRY_FILE);
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
@ -106,8 +111,34 @@ pub async fn load_registries(data_dir: &Path) -> Result<RegistryConfig> {
|
|||||||
let content = fs::read_to_string(&path)
|
let content = fs::read_to_string(&path)
|
||||||
.await
|
.await
|
||||||
.context("Failed to read registry config")?;
|
.context("Failed to read registry config")?;
|
||||||
let config: RegistryConfig =
|
let mut config: RegistryConfig =
|
||||||
serde_json::from_str(&content).unwrap_or_else(|_| RegistryConfig::default());
|
serde_json::from_str(&content).unwrap_or_else(|_| RegistryConfig::default());
|
||||||
|
|
||||||
|
// Migrate: any default registry URL that isn't already in the
|
||||||
|
// saved list gets appended at the end (so existing priority order
|
||||||
|
// is preserved for anything the operator already configured).
|
||||||
|
let defaults = RegistryConfig::default();
|
||||||
|
let known: std::collections::HashSet<String> =
|
||||||
|
config.registries.iter().map(|r| r.url.clone()).collect();
|
||||||
|
let max_priority = config
|
||||||
|
.registries
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.priority)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0);
|
||||||
|
let mut added = false;
|
||||||
|
for (i, def) in defaults.registries.iter().enumerate() {
|
||||||
|
if !known.contains(&def.url) {
|
||||||
|
let mut cloned = def.clone();
|
||||||
|
cloned.priority = max_priority.saturating_add(10 + i as u32);
|
||||||
|
config.registries.push(cloned);
|
||||||
|
added = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if added {
|
||||||
|
// Persist so the next load doesn't have to re-merge.
|
||||||
|
let _ = save_registries(data_dir, &config).await;
|
||||||
|
}
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -68,6 +68,10 @@ const DEFAULT_UPDATE_MANIFEST_URL: &str =
|
|||||||
/// is slow or unreachable.
|
/// is slow or unreachable.
|
||||||
const DEFAULT_SECONDARY_MIRROR_URL: &str =
|
const DEFAULT_SECONDARY_MIRROR_URL: &str =
|
||||||
"http://23.182.128.160:3000/lfg2025/archy/raw/branch/main/releases/manifest.json";
|
"http://23.182.128.160:3000/lfg2025/archy/raw/branch/main/releases/manifest.json";
|
||||||
|
/// Tertiary mirror on a separate OVH VPS — independent network path so
|
||||||
|
/// a single-provider outage doesn't knock out all three mirrors.
|
||||||
|
const DEFAULT_TERTIARY_MIRROR_URL: &str =
|
||||||
|
"http://146.59.87.168:3000/lfg2025/archy/raw/branch/main/releases/manifest.json";
|
||||||
const UPDATE_STATE_FILE: &str = "update_state.json";
|
const UPDATE_STATE_FILE: &str = "update_state.json";
|
||||||
const UPDATE_MIRRORS_FILE: &str = "update-mirrors.json";
|
const UPDATE_MIRRORS_FILE: &str = "update-mirrors.json";
|
||||||
|
|
||||||
@ -97,13 +101,24 @@ fn default_mirrors() -> Vec<UpdateMirror> {
|
|||||||
url: DEFAULT_UPDATE_MANIFEST_URL.to_string(),
|
url: DEFAULT_UPDATE_MANIFEST_URL.to_string(),
|
||||||
label: "Server 2 (tx1138)".to_string(),
|
label: "Server 2 (tx1138)".to_string(),
|
||||||
},
|
},
|
||||||
|
UpdateMirror {
|
||||||
|
url: DEFAULT_TERTIARY_MIRROR_URL.to_string(),
|
||||||
|
label: "Server 3 (OVH)".to_string(),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the operator-configured mirror list. Returns defaults if the
|
/// Load the operator-configured mirror list. Returns defaults if the
|
||||||
/// file doesn't exist yet, so a node OTA'd from a pre-mirrors release
|
/// file doesn't exist yet, so a node OTA'd from a pre-mirrors release
|
||||||
/// starts with both Server 1 and Server 2 available without any manual
|
/// starts with the current default mirrors available without any
|
||||||
/// config.
|
/// manual config.
|
||||||
|
///
|
||||||
|
/// Migration: any default mirror URL that isn't already in the saved
|
||||||
|
/// list gets appended at the end. This lets us add new default mirrors
|
||||||
|
/// (e.g. a new Server 3) and have them appear on existing nodes after
|
||||||
|
/// an update, without requiring manual config edits. Explicit removals
|
||||||
|
/// stick — once an operator removes a URL it stays gone unless it's
|
||||||
|
/// later re-added to defaults.
|
||||||
pub async fn load_mirrors(data_dir: &Path) -> Result<Vec<UpdateMirror>> {
|
pub async fn load_mirrors(data_dir: &Path) -> Result<Vec<UpdateMirror>> {
|
||||||
let path = mirrors_path(data_dir);
|
let path = mirrors_path(data_dir);
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
@ -112,13 +127,27 @@ pub async fn load_mirrors(data_dir: &Path) -> Result<Vec<UpdateMirror>> {
|
|||||||
let bytes = fs::read(&path)
|
let bytes = fs::read(&path)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("read {}", path.display()))?;
|
.with_context(|| format!("read {}", path.display()))?;
|
||||||
let list: Vec<UpdateMirror> =
|
let mut list: Vec<UpdateMirror> =
|
||||||
serde_json::from_slice(&bytes).with_context(|| format!("parse {}", path.display()))?;
|
serde_json::from_slice(&bytes).with_context(|| format!("parse {}", path.display()))?;
|
||||||
if list.is_empty() {
|
if list.is_empty() {
|
||||||
Ok(default_mirrors())
|
return Ok(default_mirrors());
|
||||||
} else {
|
|
||||||
Ok(list)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge in any default URLs the saved config is missing.
|
||||||
|
let known: std::collections::HashSet<String> =
|
||||||
|
list.iter().map(|m| m.url.clone()).collect();
|
||||||
|
let defaults = default_mirrors();
|
||||||
|
let mut added = false;
|
||||||
|
for def in &defaults {
|
||||||
|
if !known.contains(&def.url) {
|
||||||
|
list.push(def.clone());
|
||||||
|
added = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if added {
|
||||||
|
let _ = save_mirrors(data_dir, &list).await;
|
||||||
|
}
|
||||||
|
Ok(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save_mirrors(data_dir: &Path, mirrors: &[UpdateMirror]) -> Result<()> {
|
pub async fn save_mirrors(data_dir: &Path, mirrors: &[UpdateMirror]) -> Result<()> {
|
||||||
@ -1187,9 +1216,10 @@ mod tests {
|
|||||||
async fn test_load_mirrors_returns_defaults_when_absent() {
|
async fn test_load_mirrors_returns_defaults_when_absent() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let list = load_mirrors(dir.path()).await.unwrap();
|
let list = load_mirrors(dir.path()).await.unwrap();
|
||||||
assert_eq!(list.len(), 2);
|
assert_eq!(list.len(), 3);
|
||||||
assert!(list[0].url.contains("git.tx1138.com"));
|
assert!(list[0].url.contains("23.182.128.160"));
|
||||||
assert!(list[1].url.contains("23.182.128.160"));
|
assert!(list[1].url.contains("git.tx1138.com"));
|
||||||
|
assert!(list[2].url.contains("146.59.87.168"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@ -1,27 +1,28 @@
|
|||||||
{
|
{
|
||||||
"version": "1.7.30-alpha",
|
"version": "1.7.31-alpha",
|
||||||
"release_date": "2026-04-21",
|
"release_date": "2026-04-22",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"App installs now show a real download progress bar — same accuracy as the system update bar. You'll see 'Downloading: 50.5 / 200.0 MB (25%)' with a live percentage instead of a generic spinner. The bar keeps streaming even when the install falls back from one registry to another, so you'll never see a 'stuck at 0%' again.",
|
"Installing IndeedHub is now fully self-healing — if a previous install was interrupted, re-running from the App Store automatically cleans up the leftover containers and tries again instead of failing with a 'name already in use' error.",
|
||||||
"Uninstalls now show what's actually happening: 'Stopping containers (2/5)', 'Cleaning up volumes', 'Removing app data' — labelled per app so you can fire off multiple uninstalls in parallel and watch each one's stage on its own card.",
|
"New default app registries and update mirrors now appear automatically on existing nodes after an update — no more needing to manually add Server 3 (OVH) from the settings page. Anything you've explicitly removed stays removed.",
|
||||||
"OVH (146.59.87.168) is now baked in as Server 3 by default for both updates and the app registry — extra mirror, completely independent network path so a single-provider outage can't take everything down."
|
"Fixed the 'Test' button on registries that protect their API endpoint — it used to falsely report those registries as unreachable. It now correctly recognizes a protected-but-alive registry as reachable.",
|
||||||
|
"First-boot cleanup: removed an old IndeedHub stub from the first-boot script that used to race the main installer and occasionally leave a half-installed IndeedHub behind."
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"name": "archipelago",
|
"name": "archipelago",
|
||||||
"current_version": "1.7.29-alpha",
|
"current_version": "1.7.30-alpha",
|
||||||
"new_version": "1.7.30-alpha",
|
"new_version": "1.7.31-alpha",
|
||||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.30-alpha/archipelago",
|
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.31-alpha/archipelago",
|
||||||
"sha256": "1ec270c338f85dc9dd8de0d06f8a9a342808543859c732db8c3212c1d0eb598d",
|
"sha256": "ce2f899d3c4b615136223ae6295ee4b5de4009d1db926f7648a788c0ad3c84b8",
|
||||||
"size_bytes": 40916480
|
"size_bytes": 40786728
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "archipelago-frontend-1.7.30-alpha.tar.gz",
|
"name": "archipelago-frontend-1.7.31-alpha.tar.gz",
|
||||||
"current_version": "1.7.29-alpha",
|
"current_version": "1.7.30-alpha",
|
||||||
"new_version": "1.7.30-alpha",
|
"new_version": "1.7.31-alpha",
|
||||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.30-alpha/archipelago-frontend-1.7.30-alpha.tar.gz",
|
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.31-alpha/archipelago-frontend-1.7.31-alpha.tar.gz",
|
||||||
"sha256": "8f2a56cc08f648b1ff0c4181b0a2d19b3e3b6a599166cfe15f1eb585282bb552",
|
"sha256": "00f474725edaf14dc41d0c02abd3afcff1b30fa50846adec9e11b3c5b2188564",
|
||||||
"size_bytes": 77007976
|
"size_bytes": 77008771
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
releases/v1.7.31-alpha/archipelago
Executable file
BIN
releases/v1.7.31-alpha/archipelago
Executable file
Binary file not shown.
BIN
releases/v1.7.31-alpha/archipelago-frontend-1.7.31-alpha.tar.gz
Normal file
BIN
releases/v1.7.31-alpha/archipelago-frontend-1.7.31-alpha.tar.gz
Normal file
Binary file not shown.
@ -1216,44 +1216,6 @@ if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'str
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 8b. Indeehub (pull from registry, or use local build)
|
|
||||||
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q indeedhub; then
|
|
||||||
# Use image-versions.sh variable if sourced, otherwise detect
|
|
||||||
if [ -z "${INDEEDHUB_IMAGE:-}" ]; then
|
|
||||||
INDEEDHUB_IMAGE=""
|
|
||||||
# Try local image first (pre-built or loaded from ISO)
|
|
||||||
if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'localhost/indeedhub'; then
|
|
||||||
INDEEDHUB_IMAGE="localhost/indeedhub:local"
|
|
||||||
# Try pinned registry image
|
|
||||||
elif $DOCKER pull "$ARCHY_REGISTRY/indeedhub:1.0.0" --tls-verify=false 2>>"$LOG"; then
|
|
||||||
INDEEDHUB_IMAGE="$ARCHY_REGISTRY/indeedhub:1.0.0"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [ -n "$INDEEDHUB_IMAGE" ]; then
|
|
||||||
log "Creating Indeehub from $INDEEDHUB_IMAGE..."
|
|
||||||
$DOCKER run -d --name indeedhub --restart unless-stopped \
|
|
||||||
--network archy-net --network-alias indeedhub \
|
|
||||||
--health-cmd="curl -sf http://localhost:7777/health || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
|
|
||||||
--memory=$(mem_limit indeedhub) \
|
|
||||||
--cap-drop ALL --security-opt no-new-privileges:true \
|
|
||||||
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
|
|
||||||
-p 7778:7777 \
|
|
||||||
"$INDEEDHUB_IMAGE" 2>>"$LOG" || true
|
|
||||||
# Fix IndeedHub for iframe: remove X-Frame-Options so it loads in Archipelago panel
|
|
||||||
sleep 2
|
|
||||||
if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q "^indeedhub$"; then
|
|
||||||
$DOCKER exec indeedhub sed -i "/X-Frame-Options/d" /etc/nginx/conf.d/default.conf 2>/dev/null || true
|
|
||||||
# Fix Host header for NIP-98 auth — $host strips port, $http_host preserves it
|
|
||||||
$DOCKER exec indeedhub sed -i 's|proxy_set_header Host $host;|proxy_set_header Host $http_host;|g' /etc/nginx/conf.d/default.conf 2>/dev/null || true
|
|
||||||
if [ -f /opt/archipelago/web-ui/nostr-provider.js ]; then
|
|
||||||
$DOCKER cp /opt/archipelago/web-ui/nostr-provider.js indeedhub:/usr/share/nginx/html/nostr-provider.js 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
$DOCKER exec indeedhub nginx -s reload 2>/dev/null || true
|
|
||||||
log "Applied IndeedHub iframe fix (X-Frame-Options, Host header, nostr-provider)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 9. Custom UI containers (bitcoin-ui, lnd-ui)
|
# 9. Custom UI containers (bitcoin-ui, lnd-ui)
|
||||||
# These are built from Dockerfiles in /opt/archipelago/docker/ or loaded from pre-built images.
|
# These are built from Dockerfiles in /opt/archipelago/docker/ or loaded from pre-built images.
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user