- New shell_escape() utility for safe command interpolation - Applied across all MCP tools, docker, scanner, network commands - MCP server generates random bearer token at startup - Token written to mcp-token file with 0600 permissions - All MCP HTTP requests require Authorization header - Bridge binary reads token and sends on every request Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
118 lines
5.0 KiB
Rust
118 lines
5.0 KiB
Rust
//! Tauri commands for Docker management via SSH exec channels.
|
|
|
|
use tauri::State;
|
|
use serde::Serialize;
|
|
use crate::AppState;
|
|
use crate::utils::shell_escape;
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct DockerContainer {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub image: String,
|
|
pub status: String,
|
|
pub ports: String,
|
|
pub created: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct DockerImage {
|
|
pub id: String,
|
|
pub repository: String,
|
|
pub tag: String,
|
|
pub size: String,
|
|
pub created: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct DockerVolume {
|
|
pub name: String,
|
|
pub driver: String,
|
|
pub mountpoint: String,
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn docker_list_containers(session_id: String, all: Option<bool>, state: State<'_, AppState>) -> Result<Vec<DockerContainer>, String> {
|
|
let session = state.ssh.get_session(&session_id).ok_or("Session not found")?;
|
|
let flag = if all.unwrap_or(true) { "-a" } else { "" };
|
|
let output = exec(&session.handle, &format!("docker ps {} --format '{{{{.ID}}}}|{{{{.Names}}}}|{{{{.Image}}}}|{{{{.Status}}}}|{{{{.Ports}}}}|{{{{.CreatedAt}}}}' 2>&1", flag)).await?;
|
|
Ok(output.lines().filter(|l| !l.is_empty() && !l.starts_with("CONTAINER")).map(|line| {
|
|
let p: Vec<&str> = line.splitn(6, '|').collect();
|
|
DockerContainer {
|
|
id: p.first().unwrap_or(&"").to_string(),
|
|
name: p.get(1).unwrap_or(&"").to_string(),
|
|
image: p.get(2).unwrap_or(&"").to_string(),
|
|
status: p.get(3).unwrap_or(&"").to_string(),
|
|
ports: p.get(4).unwrap_or(&"").to_string(),
|
|
created: p.get(5).unwrap_or(&"").to_string(),
|
|
}
|
|
}).collect())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn docker_list_images(session_id: String, state: State<'_, AppState>) -> Result<Vec<DockerImage>, String> {
|
|
let session = state.ssh.get_session(&session_id).ok_or("Session not found")?;
|
|
let output = exec(&session.handle, "docker images --format '{{.ID}}|{{.Repository}}|{{.Tag}}|{{.Size}}|{{.CreatedAt}}' 2>&1").await?;
|
|
Ok(output.lines().filter(|l| !l.is_empty()).map(|line| {
|
|
let p: Vec<&str> = line.splitn(5, '|').collect();
|
|
DockerImage {
|
|
id: p.first().unwrap_or(&"").to_string(),
|
|
repository: p.get(1).unwrap_or(&"").to_string(),
|
|
tag: p.get(2).unwrap_or(&"").to_string(),
|
|
size: p.get(3).unwrap_or(&"").to_string(),
|
|
created: p.get(4).unwrap_or(&"").to_string(),
|
|
}
|
|
}).collect())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn docker_list_volumes(session_id: String, state: State<'_, AppState>) -> Result<Vec<DockerVolume>, String> {
|
|
let session = state.ssh.get_session(&session_id).ok_or("Session not found")?;
|
|
let output = exec(&session.handle, "docker volume ls --format '{{.Name}}|{{.Driver}}|{{.Mountpoint}}' 2>&1").await?;
|
|
Ok(output.lines().filter(|l| !l.is_empty()).map(|line| {
|
|
let p: Vec<&str> = line.splitn(3, '|').collect();
|
|
DockerVolume {
|
|
name: p.first().unwrap_or(&"").to_string(),
|
|
driver: p.get(1).unwrap_or(&"").to_string(),
|
|
mountpoint: p.get(2).unwrap_or(&"").to_string(),
|
|
}
|
|
}).collect())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn docker_action(session_id: String, action: String, target: String, state: State<'_, AppState>) -> Result<String, String> {
|
|
let session = state.ssh.get_session(&session_id).ok_or("Session not found")?;
|
|
let t = shell_escape(&target);
|
|
let cmd = match action.as_str() {
|
|
"start" => format!("docker start {} 2>&1", t),
|
|
"stop" => format!("docker stop {} 2>&1", t),
|
|
"restart" => format!("docker restart {} 2>&1", t),
|
|
"remove" => format!("docker rm -f {} 2>&1", t),
|
|
"logs" => format!("docker logs --tail 100 {} 2>&1", t),
|
|
"remove-image" => format!("docker rmi {} 2>&1", t),
|
|
"remove-volume" => format!("docker volume rm {} 2>&1", t),
|
|
"builder-prune" => "docker builder prune -f 2>&1".to_string(),
|
|
"system-prune" => "docker system prune -f 2>&1".to_string(),
|
|
"system-prune-all" => "docker system prune -a -f 2>&1".to_string(),
|
|
_ => return Err(format!("Unknown docker action: {}", action)),
|
|
};
|
|
exec(&session.handle, &cmd).await
|
|
}
|
|
|
|
async fn exec(handle: &std::sync::Arc<tokio::sync::Mutex<russh::client::Handle<crate::ssh::session::SshClient>>>, cmd: &str) -> Result<String, String> {
|
|
let mut channel = { let h = handle.lock().await; h.channel_open_session().await.map_err(|e| format!("Exec failed: {}", e))? };
|
|
channel.exec(true, cmd).await.map_err(|e| format!("Exec failed: {}", e))?;
|
|
let mut output = String::new();
|
|
loop {
|
|
match channel.wait().await {
|
|
Some(russh::ChannelMsg::Data { ref data }) => { if let Ok(t) = std::str::from_utf8(data.as_ref()) { output.push_str(t); } }
|
|
Some(russh::ChannelMsg::Eof) | Some(russh::ChannelMsg::Close) | None => break,
|
|
_ => {}
|
|
}
|
|
}
|
|
Ok(output)
|
|
}
|