Implement bundled app management in RPC and UI
- Added new RPC methods for starting and stopping bundled apps, allowing management of pre-loaded container images. - Enhanced container listing logic to include a fallback to Podman for bundled apps. - Updated the UI to display bundled apps with their respective statuses, including start and stop functionality. - Introduced a new Pinia store structure to manage loading states and app statuses for bundled applications. - Refactored existing components to improve user experience and streamline app management.
This commit is contained in:
@@ -89,6 +89,10 @@ impl RpcHandler {
|
||||
"package.stop" => self.handle_package_stop(rpc_req.params).await,
|
||||
"package.restart" => self.handle_package_restart(rpc_req.params).await,
|
||||
|
||||
// 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,
|
||||
|
||||
_ => {
|
||||
Err(anyhow::anyhow!("Unknown method: {}", rpc_req.method))
|
||||
}
|
||||
@@ -273,17 +277,64 @@ impl RpcHandler {
|
||||
}
|
||||
|
||||
async fn handle_container_list(&self) -> Result<serde_json::Value> {
|
||||
let orchestrator = self
|
||||
.orchestrator
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?;
|
||||
// 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)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let containers = orchestrator
|
||||
.list_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")?;
|
||||
.context("Failed to list containers via podman")?;
|
||||
|
||||
Ok(serde_json::to_value(containers)?)
|
||||
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!([]));
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
async fn handle_container_status(
|
||||
@@ -469,4 +520,116 @@ impl RpcHandler {
|
||||
|
||||
Ok(serde_json::Value::Null)
|
||||
}
|
||||
|
||||
/// 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 }))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user