use super::RpcHandler; use anyhow::{Context, Result}; impl RpcHandler { pub(super) async fn handle_container_install( &self, params: Option, ) -> Result { let orchestrator = self .orchestrator .as_ref() .ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?; let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let manifest_path = params .get("manifest_path") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing manifest_path"))?; // Validate manifest path: reject path traversal and paths outside apps/ if manifest_path.contains("..") { return Err(anyhow::anyhow!( "Invalid manifest_path: path traversal not allowed" )); } let path = std::path::Path::new(manifest_path); if path.is_absolute() { let apps_dir = self.config.data_dir.join("apps"); if !path.starts_with(&apps_dir) { return Err(anyhow::anyhow!( "Invalid manifest_path: must be under the apps directory" )); } } // Load manifest let manifest_content = tokio::fs::read_to_string(manifest_path) .await .context("Failed to read manifest file")?; let manifest: archipelago_container::AppManifest = serde_yaml::from_str(&manifest_content) .context("Failed to parse manifest")?; let container_name = orchestrator .install_container(&manifest, manifest_path) .await .context("Failed to install container")?; Ok(serde_json::json!(container_name)) } pub(super) async fn handle_container_start( &self, params: Option, ) -> Result { let orchestrator = self .orchestrator .as_ref() .ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?; let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let app_id = params .get("app_id") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing app_id"))?; orchestrator .start_container(app_id) .await .context("Failed to start container")?; Ok(serde_json::json!({ "status": "started" })) } pub(super) async fn handle_container_stop( &self, params: Option, ) -> Result { let orchestrator = self .orchestrator .as_ref() .ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?; let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let app_id = params .get("app_id") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing app_id"))?; orchestrator .stop_container(app_id) .await .context("Failed to stop container")?; Ok(serde_json::json!({ "status": "stopped" })) } pub(super) async fn handle_container_remove( &self, params: Option, ) -> Result { let orchestrator = self .orchestrator .as_ref() .ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?; let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let app_id = params .get("app_id") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing app_id"))?; let preserve_data = params .get("preserve_data") .and_then(|v| v.as_bool()) .unwrap_or(false); orchestrator .remove_container(app_id, preserve_data) .await .context("Failed to remove container")?; Ok(serde_json::json!({ "status": "removed" })) } pub(super) async fn handle_container_list(&self) -> Result { // Try to get containers from orchestrator first if let Some(orchestrator) = &self.orchestrator { if let Ok(containers) = orchestrator.list_containers().await { if !containers.is_empty() { return Ok(serde_json::to_value(containers)?); } } } // Fallback: list containers directly via sudo podman (for bundled apps) let output = tokio::process::Command::new("sudo") .args(["podman", "ps", "-a", "--format", "json"]) .output() .await .context("Failed to list containers via podman")?; if !output.status.success() { return Ok(serde_json::json!([])); } let stdout = String::from_utf8_lossy(&output.stdout); if stdout.trim().is_empty() { return Ok(serde_json::json!([])); } // Parse podman JSON output let podman_containers: Vec = serde_json::from_str(&stdout) .unwrap_or_else(|_| Vec::new()); // Convert to our ContainerStatus format let containers: Vec = podman_containers .iter() .map(|c| { let state = c.get("State").and_then(|v| v.as_str()).unwrap_or("unknown"); let mapped_state = match state.to_lowercase().as_str() { "running" => "running", "exited" => "exited", "stopped" => "stopped", "created" => "created", "paused" => "paused", _ => "unknown", }; let name = c.get("Names").and_then(|v| v.as_array()).and_then(|a| a.first()).and_then(|v| v.as_str()).unwrap_or(""); // Determine lan_address based on container name let lan_address = match name { "bitcoin-knots" => Some("http://localhost:8334"), "lnd" => Some("http://localhost:8081"), "tailscale" => Some("http://localhost:8240"), _ => None, }; serde_json::json!({ "id": c.get("Id").and_then(|v| v.as_str()).unwrap_or(""), "name": name, "state": mapped_state, "image": c.get("Image").and_then(|v| v.as_str()).unwrap_or(""), "created": c.get("Created").and_then(|v| v.as_str()).unwrap_or(""), "ports": c.get("Ports").and_then(|v| v.as_array()).map(|a| a.iter().filter_map(|p| p.get("hostPort").and_then(|v| v.as_u64()).map(|p| p.to_string())).collect::>() ).unwrap_or_default(), "lan_address": lan_address, }) }) .collect(); Ok(serde_json::json!(containers)) } pub(super) async fn handle_container_status( &self, params: Option, ) -> Result { let orchestrator = self .orchestrator .as_ref() .ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?; let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let app_id = params .get("app_id") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing app_id"))?; let status = orchestrator .get_container_status(app_id) .await .context("Failed to get container status")?; Ok(serde_json::to_value(status)?) } pub(super) async fn handle_container_logs( &self, params: Option, ) -> Result { let orchestrator = self .orchestrator .as_ref() .ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?; let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let app_id = params .get("app_id") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing app_id"))?; let lines = params .get("lines") .and_then(|v| v.as_u64()) .unwrap_or(100) as u32; let logs = orchestrator .get_container_logs(app_id, lines) .await .context("Failed to get container logs")?; Ok(serde_json::to_value(logs)?) } /// Used by HTTP GET /api/container/logs (same logic as container-logs RPC). pub async fn get_container_logs_value( &self, app_id: &str, lines: u32, ) -> Result { let orchestrator = self .orchestrator .as_ref() .ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?; let logs = orchestrator .get_container_logs(app_id, lines) .await .context("Failed to get container logs")?; Ok(serde_json::to_value(logs)?) } pub(super) async fn handle_container_health( &self, params: Option, ) -> Result { let orchestrator = self .orchestrator .as_ref() .ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?; // If app_id is provided, get health for that app if let Some(params) = params { if let Some(app_id) = params.get("app_id").and_then(|v| v.as_str()) { let health = orchestrator .get_health_status(app_id) .await .context("Failed to get container health")?; return Ok(serde_json::json!({ app_id: health })); } } // Otherwise, get health for all containers let containers = orchestrator .list_containers() .await .context("Failed to list containers")?; let mut health_map = serde_json::Map::new(); for container in containers { if let Some(app_id) = container.name.strip_prefix("archipelago-") { if let Some(app_id) = app_id.strip_suffix("-dev") { match orchestrator.get_health_status(app_id).await { Ok(health) => { health_map.insert(app_id.to_string(), serde_json::Value::String(health)); } Err(_) => { health_map.insert(app_id.to_string(), serde_json::Value::String("unknown".to_string())); } } } } } Ok(serde_json::Value::Object(health_map)) } }