2026-01-27 22:32:18 +00:00
|
|
|
use crate::auth::AuthManager;
|
2026-01-24 22:59:20 +00:00
|
|
|
use crate::config::Config;
|
|
|
|
|
use crate::container::DevContainerOrchestrator;
|
|
|
|
|
use anyhow::{Context, Result};
|
|
|
|
|
use hyper::{Request, Response, StatusCode};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use tracing::{debug, error};
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
struct RpcRequest {
|
|
|
|
|
method: String,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize)]
|
|
|
|
|
struct RpcResponse {
|
|
|
|
|
result: Option<serde_json::Value>,
|
|
|
|
|
error: Option<RpcError>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize)]
|
|
|
|
|
struct RpcError {
|
|
|
|
|
code: i32,
|
|
|
|
|
message: String,
|
|
|
|
|
data: Option<serde_json::Value>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 22:32:18 +00:00
|
|
|
/// Default dev password when no user is set up (matches mock-backend).
|
|
|
|
|
const DEV_DEFAULT_PASSWORD: &str = "password123";
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
pub struct RpcHandler {
|
2026-01-28 11:03:34 +00:00
|
|
|
config: Config,
|
2026-01-27 22:32:18 +00:00
|
|
|
auth_manager: AuthManager,
|
2026-01-24 22:59:20 +00:00
|
|
|
orchestrator: Option<Arc<DevContainerOrchestrator>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RpcHandler {
|
|
|
|
|
pub async fn new(config: Config) -> Result<Self> {
|
2026-01-27 22:32:18 +00:00
|
|
|
let auth_manager = AuthManager::new(config.data_dir.clone());
|
2026-01-24 22:59:20 +00:00
|
|
|
let orchestrator = if config.dev_mode {
|
|
|
|
|
Some(Arc::new(
|
|
|
|
|
DevContainerOrchestrator::new(config.clone()).await?,
|
|
|
|
|
))
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(Self {
|
2026-01-28 11:03:34 +00:00
|
|
|
config,
|
2026-01-27 22:32:18 +00:00
|
|
|
auth_manager,
|
2026-01-24 22:59:20 +00:00
|
|
|
orchestrator,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn handle(
|
|
|
|
|
&self,
|
Update archipelago: API, auth, container, parmanode, performance, security
- API handler, RPC, and server updates
- Auth and coding rules
- Container data manager, dev orchestrator, health monitor, podman client
- Parmanode script runner
- Performance resource manager
- Security container policies and secrets manager
- Add build scripts and documentation
2026-01-27 22:27:17 +00:00
|
|
|
req: Request<hyper::Body>,
|
|
|
|
|
) -> Result<Response<hyper::Body>> {
|
|
|
|
|
// Read request body
|
2026-01-24 22:59:20 +00:00
|
|
|
let (_, body) = req.into_parts();
|
Update archipelago: API, auth, container, parmanode, performance, security
- API handler, RPC, and server updates
- Auth and coding rules
- Container data manager, dev orchestrator, health monitor, podman client
- Parmanode script runner
- Performance resource manager
- Security container policies and secrets manager
- Add build scripts and documentation
2026-01-27 22:27:17 +00:00
|
|
|
let body_bytes = hyper::body::to_bytes(body).await
|
|
|
|
|
.context("Failed to read body")?;
|
2026-01-24 22:59:20 +00:00
|
|
|
|
|
|
|
|
let rpc_req: RpcRequest = serde_json::from_slice(&body_bytes)
|
|
|
|
|
.context("Invalid RPC request")?;
|
|
|
|
|
|
|
|
|
|
debug!("RPC method: {}", rpc_req.method);
|
|
|
|
|
|
|
|
|
|
// Route to handler
|
|
|
|
|
let result = match rpc_req.method.as_str() {
|
|
|
|
|
"echo" => self.handle_echo(rpc_req.params).await,
|
|
|
|
|
"server.echo" => self.handle_echo(rpc_req.params).await,
|
2026-01-27 22:32:18 +00:00
|
|
|
"auth.login" => self.handle_auth_login(rpc_req.params).await,
|
2026-01-27 22:55:20 +00:00
|
|
|
"auth.logout" => self.handle_auth_logout().await,
|
2026-01-27 23:21:26 +00:00
|
|
|
|
|
|
|
|
// Container orchestration (for Archipelago-managed containers)
|
2026-01-24 22:59:20 +00:00
|
|
|
"container-install" => self.handle_container_install(rpc_req.params).await,
|
|
|
|
|
"container-start" => self.handle_container_start(rpc_req.params).await,
|
|
|
|
|
"container-stop" => self.handle_container_stop(rpc_req.params).await,
|
|
|
|
|
"container-remove" => self.handle_container_remove(rpc_req.params).await,
|
|
|
|
|
"container-list" => self.handle_container_list().await,
|
|
|
|
|
"container-status" => self.handle_container_status(rpc_req.params).await,
|
|
|
|
|
"container-logs" => self.handle_container_logs(rpc_req.params).await,
|
|
|
|
|
"container-health" => self.handle_container_health(rpc_req.params).await,
|
2026-01-27 23:21:26 +00:00
|
|
|
|
|
|
|
|
// Package management (for docker-compose apps)
|
|
|
|
|
"package.start" => self.handle_package_start(rpc_req.params).await,
|
|
|
|
|
"package.stop" => self.handle_package_stop(rpc_req.params).await,
|
|
|
|
|
"package.restart" => self.handle_package_restart(rpc_req.params).await,
|
|
|
|
|
|
2026-02-01 06:04:36 +00:00
|
|
|
// Bundled app management (for pre-loaded container images)
|
|
|
|
|
"bundled-app-start" => self.handle_bundled_app_start(rpc_req.params).await,
|
|
|
|
|
"bundled-app-stop" => self.handle_bundled_app_stop(rpc_req.params).await,
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
_ => {
|
|
|
|
|
Err(anyhow::anyhow!("Unknown method: {}", rpc_req.method))
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Build response
|
|
|
|
|
let rpc_resp = match result {
|
|
|
|
|
Ok(data) => RpcResponse {
|
|
|
|
|
result: Some(data),
|
|
|
|
|
error: None,
|
|
|
|
|
},
|
|
|
|
|
Err(e) => {
|
|
|
|
|
error!("RPC error: {}", e);
|
|
|
|
|
RpcResponse {
|
|
|
|
|
result: None,
|
|
|
|
|
error: Some(RpcError {
|
|
|
|
|
code: -1,
|
|
|
|
|
message: e.to_string(),
|
|
|
|
|
data: None,
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let body = serde_json::to_vec(&rpc_resp)
|
|
|
|
|
.context("Failed to serialize response")?;
|
|
|
|
|
|
|
|
|
|
Ok(Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.header("Content-Type", "application/json")
|
Update archipelago: API, auth, container, parmanode, performance, security
- API handler, RPC, and server updates
- Auth and coding rules
- Container data manager, dev orchestrator, health monitor, podman client
- Parmanode script runner
- Performance resource manager
- Security container policies and secrets manager
- Add build scripts and documentation
2026-01-27 22:27:17 +00:00
|
|
|
.body(hyper::Body::from(body))
|
2026-01-24 22:59:20 +00:00
|
|
|
.unwrap())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn handle_echo(&self, params: Option<serde_json::Value>) -> Result<serde_json::Value> {
|
|
|
|
|
if let Some(p) = params {
|
|
|
|
|
if let Some(msg) = p.get("message").and_then(|v| v.as_str()) {
|
|
|
|
|
return Ok(serde_json::json!({ "message": msg }));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(serde_json::json!({ "message": "Hello from Archipelago!" }))
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 22:32:18 +00:00
|
|
|
async fn handle_auth_login(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
|
|
|
|
let password = params
|
|
|
|
|
.get("password")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing password"))?;
|
|
|
|
|
|
|
|
|
|
let is_setup = self.auth_manager.is_setup().await?;
|
|
|
|
|
if !is_setup {
|
|
|
|
|
// Dev mode: allow default password so UI can log in without running setup
|
2026-01-28 11:03:34 +00:00
|
|
|
if self.config.dev_mode && password == DEV_DEFAULT_PASSWORD {
|
2026-01-27 22:32:18 +00:00
|
|
|
return Ok(serde_json::Value::Null);
|
|
|
|
|
}
|
|
|
|
|
return Err(anyhow::anyhow!(
|
|
|
|
|
"User not set up. Please complete setup first."
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let valid = self.auth_manager.verify_password(password).await?;
|
|
|
|
|
if !valid {
|
|
|
|
|
return Err(anyhow::anyhow!("Password Incorrect"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(serde_json::Value::Null)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 22:55:20 +00:00
|
|
|
async fn handle_auth_logout(&self) -> Result<serde_json::Value> {
|
|
|
|
|
// For now, just return success. In a full implementation, this would:
|
|
|
|
|
// - Invalidate session tokens
|
|
|
|
|
// - Clear cookies (if we were managing them)
|
|
|
|
|
// - Close authenticated WebSocket connections
|
|
|
|
|
Ok(serde_json::Value::Null)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
async fn handle_container_install(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
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"))?;
|
|
|
|
|
|
|
|
|
|
// 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))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn handle_container_start(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
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" }))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn handle_container_stop(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
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" }))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn handle_container_remove(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
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" }))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn handle_container_list(&self) -> Result<serde_json::Value> {
|
2026-02-01 06:04:36 +00:00
|
|
|
// 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)?);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-24 22:59:20 +00:00
|
|
|
|
2026-02-01 06:04:36 +00:00
|
|
|
// Fallback: list containers directly via sudo podman (for bundled apps)
|
|
|
|
|
let output = tokio::process::Command::new("sudo")
|
|
|
|
|
.args(["podman", "ps", "-a", "--format", "json"])
|
|
|
|
|
.output()
|
2026-01-24 22:59:20 +00:00
|
|
|
.await
|
2026-02-01 06:04:36 +00:00
|
|
|
.context("Failed to list containers via podman")?;
|
|
|
|
|
|
|
|
|
|
if !output.status.success() {
|
|
|
|
|
// If podman fails, return empty list
|
|
|
|
|
return Ok(serde_json::json!([]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
|
|
|
if stdout.trim().is_empty() {
|
|
|
|
|
return Ok(serde_json::json!([]));
|
|
|
|
|
}
|
2026-01-24 22:59:20 +00:00
|
|
|
|
2026-02-01 06:04:36 +00:00
|
|
|
// Parse podman JSON output
|
|
|
|
|
let podman_containers: Vec<serde_json::Value> = serde_json::from_str(&stdout)
|
|
|
|
|
.unwrap_or_else(|_| Vec::new());
|
|
|
|
|
|
|
|
|
|
// Convert to our ContainerStatus format
|
|
|
|
|
let containers: Vec<serde_json::Value> = 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",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"id": c.get("Id").and_then(|v| v.as_str()).unwrap_or(""),
|
|
|
|
|
"name": c.get("Names").and_then(|v| v.as_array()).and_then(|a| a.first()).and_then(|v| v.as_str()).unwrap_or(""),
|
|
|
|
|
"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::<Vec<_>>()
|
|
|
|
|
).unwrap_or_default(),
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
Ok(serde_json::json!(containers))
|
2026-01-24 22:59:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn handle_container_status(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
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)?)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn handle_container_logs(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
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)?)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn handle_container_health(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
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 {
|
|
|
|
|
// Extract app_id from container name
|
|
|
|
|
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))
|
|
|
|
|
}
|
2026-01-27 23:21:26 +00:00
|
|
|
|
|
|
|
|
// Package management methods for docker-compose containers
|
|
|
|
|
async fn handle_package_start(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
|
|
|
|
let package_id = params
|
|
|
|
|
.get("id")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing package id"))?;
|
|
|
|
|
|
|
|
|
|
// Convert package ID to container name (e.g., "bitcoin" -> "archy-bitcoin")
|
|
|
|
|
let container_name = format!("archy-{}", package_id);
|
|
|
|
|
|
|
|
|
|
// Use docker CLI to start the container
|
|
|
|
|
let output = tokio::process::Command::new("docker")
|
|
|
|
|
.arg("start")
|
|
|
|
|
.arg(&container_name)
|
|
|
|
|
.output()
|
|
|
|
|
.await
|
|
|
|
|
.context("Failed to execute docker start")?;
|
|
|
|
|
|
|
|
|
|
if !output.status.success() {
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
|
return Err(anyhow::anyhow!("Failed to start container: {}", stderr));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(serde_json::Value::Null)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn handle_package_stop(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
|
|
|
|
let package_id = params
|
|
|
|
|
.get("id")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing package id"))?;
|
|
|
|
|
|
|
|
|
|
// Convert package ID to container name
|
|
|
|
|
let container_name = format!("archy-{}", package_id);
|
|
|
|
|
|
|
|
|
|
// Use docker CLI to stop the container
|
|
|
|
|
let output = tokio::process::Command::new("docker")
|
|
|
|
|
.arg("stop")
|
|
|
|
|
.arg(&container_name)
|
|
|
|
|
.output()
|
|
|
|
|
.await
|
|
|
|
|
.context("Failed to execute docker stop")?;
|
|
|
|
|
|
|
|
|
|
if !output.status.success() {
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
|
return Err(anyhow::anyhow!("Failed to stop container: {}", stderr));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(serde_json::Value::Null)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn handle_package_restart(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
|
|
|
|
let package_id = params
|
|
|
|
|
.get("id")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing package id"))?;
|
|
|
|
|
|
|
|
|
|
// Convert package ID to container name
|
|
|
|
|
let container_name = format!("archy-{}", package_id);
|
|
|
|
|
|
|
|
|
|
// Use docker CLI to restart the container
|
|
|
|
|
let output = tokio::process::Command::new("docker")
|
|
|
|
|
.arg("restart")
|
|
|
|
|
.arg(&container_name)
|
|
|
|
|
.output()
|
|
|
|
|
.await
|
|
|
|
|
.context("Failed to execute docker restart")?;
|
|
|
|
|
|
|
|
|
|
if !output.status.success() {
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
|
return Err(anyhow::anyhow!("Failed to restart container: {}", stderr));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(serde_json::Value::Null)
|
|
|
|
|
}
|
2026-02-01 06:04:36 +00:00
|
|
|
|
|
|
|
|
/// Start a bundled app (create container from pre-loaded image if needed, then start)
|
|
|
|
|
async fn handle_bundled_app_start(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
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 image = params
|
|
|
|
|
.get("image")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing image"))?;
|
|
|
|
|
let ports = params
|
|
|
|
|
.get("ports")
|
|
|
|
|
.and_then(|v| v.as_array())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing ports"))?;
|
|
|
|
|
let volumes = params
|
|
|
|
|
.get("volumes")
|
|
|
|
|
.and_then(|v| v.as_array())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Missing volumes"))?;
|
|
|
|
|
|
|
|
|
|
// Check if container already exists
|
|
|
|
|
let check_output = tokio::process::Command::new("sudo")
|
|
|
|
|
.args(["podman", "ps", "-a", "--format", "{{.Names}}", "--filter", &format!("name={}", app_id)])
|
|
|
|
|
.output()
|
|
|
|
|
.await
|
|
|
|
|
.context("Failed to check container")?;
|
|
|
|
|
|
|
|
|
|
let existing = String::from_utf8_lossy(&check_output.stdout);
|
|
|
|
|
|
|
|
|
|
if existing.trim().is_empty() {
|
|
|
|
|
// Container doesn't exist - create it
|
|
|
|
|
let mut cmd = tokio::process::Command::new("sudo");
|
|
|
|
|
cmd.args(["podman", "run", "-d", "--name", app_id]);
|
|
|
|
|
|
|
|
|
|
// Add port mappings
|
|
|
|
|
for port in ports {
|
|
|
|
|
if let (Some(host), Some(container)) = (
|
|
|
|
|
port.get("host").and_then(|v| v.as_u64()),
|
|
|
|
|
port.get("container").and_then(|v| v.as_u64()),
|
|
|
|
|
) {
|
|
|
|
|
cmd.arg("-p").arg(format!("{}:{}", host, container));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add volume mappings
|
|
|
|
|
for volume in volumes {
|
|
|
|
|
if let (Some(host), Some(container)) = (
|
|
|
|
|
volume.get("host").and_then(|v| v.as_str()),
|
|
|
|
|
volume.get("container").and_then(|v| v.as_str()),
|
|
|
|
|
) {
|
|
|
|
|
// Create host directory if it doesn't exist
|
|
|
|
|
let _ = tokio::process::Command::new("sudo")
|
|
|
|
|
.args(["mkdir", "-p", host])
|
|
|
|
|
.output()
|
|
|
|
|
.await;
|
|
|
|
|
cmd.arg("-v").arg(format!("{}:{}", host, container));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd.arg(image);
|
|
|
|
|
|
|
|
|
|
let output = cmd.output().await.context("Failed to create container")?;
|
|
|
|
|
|
|
|
|
|
if !output.status.success() {
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
|
return Err(anyhow::anyhow!("Failed to create container: {}", stderr));
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Container exists - just start it
|
|
|
|
|
let output = tokio::process::Command::new("sudo")
|
|
|
|
|
.args(["podman", "start", app_id])
|
|
|
|
|
.output()
|
|
|
|
|
.await
|
|
|
|
|
.context("Failed to start container")?;
|
|
|
|
|
|
|
|
|
|
if !output.status.success() {
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
|
return Err(anyhow::anyhow!("Failed to start container: {}", stderr));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(serde_json::json!({ "status": "started", "app_id": app_id }))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Stop a bundled app
|
|
|
|
|
async fn handle_bundled_app_stop(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
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 output = tokio::process::Command::new("sudo")
|
|
|
|
|
.args(["podman", "stop", app_id])
|
|
|
|
|
.output()
|
|
|
|
|
.await
|
|
|
|
|
.context("Failed to stop container")?;
|
|
|
|
|
|
|
|
|
|
if !output.status.success() {
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
|
|
|
return Err(anyhow::anyhow!("Failed to stop container: {}", stderr));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(serde_json::json!({ "status": "stopped", "app_id": app_id }))
|
|
|
|
|
}
|
2026-01-24 22:59:20 +00:00
|
|
|
}
|