diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index 8c3433cf..e0e6ff87 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -87,6 +87,9 @@ impl RpcHandler { if matches!(package_id, "mempool" | "mempool-web") { return self.install_mempool_stack().await; } + if package_id == "indeedhub" { + return self.install_indeedhub_stack().await; + } // Dependency checks let deps = detect_running_deps().await?; diff --git a/core/archipelago/src/api/rpc/package/stacks.rs b/core/archipelago/src/api/rpc/package/stacks.rs index 58fe1961..1f6311fd 100644 --- a/core/archipelago/src/api/rpc/package/stacks.rs +++ b/core/archipelago/src/api/rpc/package/stacks.rs @@ -707,4 +707,178 @@ impl RpcHandler { "container_id": String::from_utf8_lossy(&run.stdout).trim().to_string(), })) } + + /// Install the IndeedHub multi-container stack. + pub(super) async fn install_indeedhub_stack(&self) -> Result { + + let registry = crate::container::registry::load_registries(&self.config.data_dir) + .await + .unwrap_or_default() + .registries + .into_iter() + .find(|r| r.enabled) + .map(|r| r.url) + .unwrap_or_else(|| "23.182.128.160:3000/lfg2025".to_string()); + + let user_tmp = format!( + "{}/.local/share/containers/tmp", + std::env::var("HOME").unwrap_or_else(|_| "/home/archipelago".to_string()) + ); + let _ = std::fs::create_dir_all(&user_tmp); + + install_log("INSTALL START: indeedhub stack").await; + info!("Installing IndeedHub stack from {}", registry); + + // Pull all images + let images = [ + format!("{}/postgres:16.13-alpine", registry), + format!("{}/redis:7.4.8-alpine", registry), + format!("{}/minio:RELEASE.2024-11-07T00-52-20Z", registry), + format!("{}/nostr-rs-relay:0.9.0", registry), + format!("{}/indeedhub-api:1.0.0", registry), + format!("{}/indeedhub-ffmpeg:1.0.0", registry), + format!("{}/indeedhub:1.0.0", registry), + ]; + + for img in &images { + info!("Pulling {}", img); + let status = tokio::process::Command::new("podman") + .args(["pull", img, "--tls-verify=false"]) + .env("TMPDIR", &user_tmp) + .status() + .await; + if !status.map(|s| s.success()).unwrap_or(false) { + tracing::warn!("Failed to pull {}", img); + } + } + + // Create indeedhub-net + let _ = tokio::process::Command::new("podman") + .args(["network", "create", "indeedhub-net"]) + .status() + .await; + + // Generate secrets + let db_pass = super::config::read_or_generate_secret("indeedhub-db-password").await; + let jwt_secret = super::config::read_or_generate_secret("indeedhub-jwt").await; + let minio_user = "indeeadmin"; + let minio_pass = super::config::read_or_generate_secret("indeedhub-minio-password").await; + + // 1. Postgres + let _ = tokio::process::Command::new("podman") + .args(["run", "-d", "--name", "indeedhub-postgres", + "--network", "indeedhub-net", "--network-alias", "postgres", + "--restart", "unless-stopped", + "-e", &format!("POSTGRES_DB=indeedhub"), + "-e", &format!("POSTGRES_USER=indeedhub"), + "-e", &format!("POSTGRES_PASSWORD={}", db_pass), + "-v", "indeedhub-postgres-data:/var/lib/postgresql/data", + &format!("{}/postgres:16.13-alpine", registry)]) + .env("TMPDIR", &user_tmp) + .status().await; + + // 2. Redis + let _ = tokio::process::Command::new("podman") + .args(["run", "-d", "--name", "indeedhub-redis", + "--network", "indeedhub-net", "--network-alias", "redis", + "--restart", "unless-stopped", + "-v", "indeedhub-redis-data:/data", + &format!("{}/redis:7.4.8-alpine", registry)]) + .env("TMPDIR", &user_tmp) + .status().await; + + // 3. MinIO + let _ = tokio::process::Command::new("podman") + .args(["run", "-d", "--name", "indeedhub-minio", + "--network", "indeedhub-net", "--network-alias", "minio", + "--restart", "unless-stopped", + "-e", &format!("MINIO_ROOT_USER={}", minio_user), + "-e", &format!("MINIO_ROOT_PASSWORD={}", minio_pass), + "-v", "indeedhub-minio-data:/data", + &format!("{}/minio:RELEASE.2024-11-07T00-52-20Z", registry), + "server", "/data"]) + .env("TMPDIR", &user_tmp) + .status().await; + + // 4. Nostr relay + let _ = tokio::process::Command::new("podman") + .args(["run", "-d", "--name", "indeedhub-relay", + "--network", "indeedhub-net", "--network-alias", "relay", + "--restart", "unless-stopped", + "-v", "indeedhub-relay-data:/usr/src/app/db", + &format!("{}/nostr-rs-relay:0.9.0", registry)]) + .env("TMPDIR", &user_tmp) + .status().await; + + // 5. API + let _ = tokio::process::Command::new("podman") + .args(["run", "-d", "--name", "indeedhub-api", + "--network", "indeedhub-net", "--network-alias", "api", + "--restart", "unless-stopped", + "-e", "PORT=4000", + "-e", &format!("DATABASE_HOST=postgres"), + "-e", &format!("DATABASE_USER=indeedhub"), + "-e", &format!("DATABASE_PASSWORD={}", db_pass), + "-e", &format!("DATABASE_NAME=indeedhub"), + "-e", &format!("REDIS_HOST=redis"), + "-e", &format!("S3_ENDPOINT=http://minio:9000"), + "-e", &format!("AWS_ACCESS_KEY={}", minio_user), + "-e", &format!("AWS_SECRET_KEY={}", minio_pass), + "-e", &format!("S3_PUBLIC_BUCKET_NAME=indeedhub-public"), + "-e", &format!("S3_PUBLIC_BUCKET_URL=/storage"), + "-e", &format!("NOSTR_JWT_SECRET={}", jwt_secret), + "-e", "ENVIRONMENT=production", + &format!("{}/indeedhub-api:1.0.0", registry)]) + .env("TMPDIR", &user_tmp) + .status().await; + + // 6. FFmpeg worker + let _ = tokio::process::Command::new("podman") + .args(["run", "-d", "--name", "indeedhub-ffmpeg", + "--network", "indeedhub-net", + "--restart", "unless-stopped", + "-e", &format!("DATABASE_HOST=postgres"), + "-e", &format!("DATABASE_USER=indeedhub"), + "-e", &format!("DATABASE_PASSWORD={}", db_pass), + "-e", &format!("DATABASE_NAME=indeedhub"), + "-e", &format!("QUEUE_HOST=redis"), + "-e", &format!("S3_ENDPOINT=http://minio:9000"), + "-e", &format!("AWS_ACCESS_KEY={}", minio_user), + "-e", &format!("AWS_SECRET_KEY={}", minio_pass), + "-e", &format!("AWS_REGION=us-east-1"), + "-e", &format!("S3_PUBLIC_BUCKET_NAME=indeedhub-public"), + "-e", "ENVIRONMENT=production", + "-e", "AES_MASTER_SECRET=0123456789abcdef0123456789abcdef", + &format!("{}/indeedhub-ffmpeg:1.0.0", registry)]) + .env("TMPDIR", &user_tmp) + .status().await; + + // Wait for backend services to start + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + + // 7. Frontend (nginx) + let run = tokio::process::Command::new("podman") + .args(["run", "-d", "--name", "indeedhub", + "--network", "indeedhub-net", + "--restart", "unless-stopped", + "-p", "7778:7777", + &format!("{}/indeedhub:1.0.0", registry)]) + .env("TMPDIR", &user_tmp) + .output() + .await + .context("Failed to create indeedhub container")?; + + if !run.status.success() { + let err = String::from_utf8_lossy(&run.stderr); + return Err(anyhow::anyhow!("IndeedHub frontend failed: {}", err)); + } + + install_log("INSTALL OK: indeedhub stack").await; + info!("IndeedHub stack installed"); + Ok(serde_json::json!({ + "success": true, + "package_id": "indeedhub", + "message": "IndeedHub stack installed (7 containers)", + })) + } }