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]]
|
||||
name = "archipelago"
|
||||
version = "1.7.30-alpha"
|
||||
version = "1.7.31-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.7.30-alpha"
|
||||
version = "1.7.31-alpha"
|
||||
edition = "2021"
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
authors = ["Archipelago Team"]
|
||||
|
||||
@ -1589,10 +1589,16 @@ server {
|
||||
.and_then(|v| v.as_bool())
|
||||
.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 {
|
||||
format!("https://{}/v2/", url)
|
||||
format!("https://{}/v2/", host)
|
||||
} else {
|
||||
format!("http://{}/v2/", url)
|
||||
format!("http://{}/v2/", host)
|
||||
};
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
@ -1604,8 +1610,11 @@ server {
|
||||
match client.get(&test_url).send().await {
|
||||
Ok(resp) => {
|
||||
let status = resp.status().as_u16();
|
||||
// 200 = open registry, 401 = auth required (both mean it exists)
|
||||
let reachable = status == 200 || status == 401;
|
||||
// Accept any "the server responded to HTTP" code as
|
||||
// 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!({
|
||||
"url": url,
|
||||
"reachable": reachable,
|
||||
|
||||
@ -937,6 +937,31 @@ impl RpcHandler {
|
||||
.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
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args(["network", "create", "indeedhub-net"])
|
||||
|
||||
@ -97,7 +97,12 @@ fn extract_image_name(image: &str) -> &str {
|
||||
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> {
|
||||
let path = data_dir.join(REGISTRY_FILE);
|
||||
if !path.exists() {
|
||||
@ -106,8 +111,34 @@ pub async fn load_registries(data_dir: &Path) -> Result<RegistryConfig> {
|
||||
let content = fs::read_to_string(&path)
|
||||
.await
|
||||
.context("Failed to read registry config")?;
|
||||
let config: RegistryConfig =
|
||||
let mut config: RegistryConfig =
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@ -68,6 +68,10 @@ const DEFAULT_UPDATE_MANIFEST_URL: &str =
|
||||
/// is slow or unreachable.
|
||||
const DEFAULT_SECONDARY_MIRROR_URL: &str =
|
||||
"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_MIRRORS_FILE: &str = "update-mirrors.json";
|
||||
|
||||
@ -97,13 +101,24 @@ fn default_mirrors() -> Vec<UpdateMirror> {
|
||||
url: DEFAULT_UPDATE_MANIFEST_URL.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
|
||||
/// 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
|
||||
/// config.
|
||||
/// starts with the current default mirrors available without any
|
||||
/// 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>> {
|
||||
let path = mirrors_path(data_dir);
|
||||
if !path.exists() {
|
||||
@ -112,13 +127,27 @@ pub async fn load_mirrors(data_dir: &Path) -> Result<Vec<UpdateMirror>> {
|
||||
let bytes = fs::read(&path)
|
||||
.await
|
||||
.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()))?;
|
||||
if list.is_empty() {
|
||||
Ok(default_mirrors())
|
||||
} else {
|
||||
Ok(list)
|
||||
return Ok(default_mirrors());
|
||||
}
|
||||
|
||||
// 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<()> {
|
||||
@ -1187,9 +1216,10 @@ mod tests {
|
||||
async fn test_load_mirrors_returns_defaults_when_absent() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let list = load_mirrors(dir.path()).await.unwrap();
|
||||
assert_eq!(list.len(), 2);
|
||||
assert!(list[0].url.contains("git.tx1138.com"));
|
||||
assert!(list[1].url.contains("23.182.128.160"));
|
||||
assert_eq!(list.len(), 3);
|
||||
assert!(list[0].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]
|
||||
|
||||
@ -1,27 +1,28 @@
|
||||
{
|
||||
"version": "1.7.30-alpha",
|
||||
"release_date": "2026-04-21",
|
||||
"version": "1.7.31-alpha",
|
||||
"release_date": "2026-04-22",
|
||||
"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.",
|
||||
"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.",
|
||||
"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."
|
||||
"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.",
|
||||
"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.",
|
||||
"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": [
|
||||
{
|
||||
"name": "archipelago",
|
||||
"current_version": "1.7.29-alpha",
|
||||
"new_version": "1.7.30-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.30-alpha/archipelago",
|
||||
"sha256": "1ec270c338f85dc9dd8de0d06f8a9a342808543859c732db8c3212c1d0eb598d",
|
||||
"size_bytes": 40916480
|
||||
"current_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.31-alpha/archipelago",
|
||||
"sha256": "ce2f899d3c4b615136223ae6295ee4b5de4009d1db926f7648a788c0ad3c84b8",
|
||||
"size_bytes": 40786728
|
||||
},
|
||||
{
|
||||
"name": "archipelago-frontend-1.7.30-alpha.tar.gz",
|
||||
"current_version": "1.7.29-alpha",
|
||||
"new_version": "1.7.30-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",
|
||||
"sha256": "8f2a56cc08f648b1ff0c4181b0a2d19b3e3b6a599166cfe15f1eb585282bb552",
|
||||
"size_bytes": 77007976
|
||||
"name": "archipelago-frontend-1.7.31-alpha.tar.gz",
|
||||
"current_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.31-alpha/archipelago-frontend-1.7.31-alpha.tar.gz",
|
||||
"sha256": "00f474725edaf14dc41d0c02abd3afcff1b30fa50846adec9e11b3c5b2188564",
|
||||
"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
|
||||
|
||||
# 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)
|
||||
# These are built from Dockerfiles in /opt/archipelago/docker/ or loaded from pre-built images.
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user