Removed unused sync podman_command/docker_command methods. Removed dead_code annotations from User and AuthManager (now actively used). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
449 lines
13 KiB
Rust
449 lines
13 KiB
Rust
use crate::manifest::AppManifest;
|
|
use crate::podman_client::{ContainerStatus, ContainerState, PodmanClient};
|
|
use anyhow::{Context, Result};
|
|
use async_trait::async_trait;
|
|
use std::process::Command;
|
|
use tokio::process::Command as TokioCommand;
|
|
|
|
#[async_trait]
|
|
pub trait ContainerRuntime: Send + Sync {
|
|
async fn pull_image(&self, image: &str, signature: Option<&str>) -> Result<()>;
|
|
async fn create_container(
|
|
&self,
|
|
manifest: &AppManifest,
|
|
name: &str,
|
|
port_offset: u16,
|
|
) -> Result<String>;
|
|
async fn start_container(&self, name: &str) -> Result<()>;
|
|
async fn stop_container(&self, name: &str) -> Result<()>;
|
|
async fn remove_container(&self, name: &str) -> Result<()>;
|
|
async fn get_container_status(&self, name: &str) -> Result<ContainerStatus>;
|
|
async fn get_container_logs(&self, name: &str, lines: u32) -> Result<Vec<String>>;
|
|
async fn list_containers(&self) -> Result<Vec<ContainerStatus>>;
|
|
}
|
|
|
|
pub struct PodmanRuntime {
|
|
client: PodmanClient,
|
|
}
|
|
|
|
impl PodmanRuntime {
|
|
pub fn new(user: String) -> Self {
|
|
Self {
|
|
client: PodmanClient::new(user),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ContainerRuntime for PodmanRuntime {
|
|
async fn pull_image(&self, image: &str, signature: Option<&str>) -> Result<()> {
|
|
self.client.pull_image(image, signature).await
|
|
}
|
|
|
|
async fn create_container(
|
|
&self,
|
|
manifest: &AppManifest,
|
|
name: &str,
|
|
port_offset: u16,
|
|
) -> Result<String> {
|
|
// Apply port offset to manifest ports
|
|
let mut dev_manifest = manifest.clone();
|
|
for port in &mut dev_manifest.app.ports {
|
|
port.host = port.host + port_offset;
|
|
}
|
|
|
|
// PodmanClient doesn't take port_offset, so we use the modified manifest
|
|
self.client.create_container(&dev_manifest, name).await
|
|
}
|
|
|
|
async fn start_container(&self, name: &str) -> Result<()> {
|
|
self.client.start_container(name).await
|
|
}
|
|
|
|
async fn stop_container(&self, name: &str) -> Result<()> {
|
|
self.client.stop_container(name).await
|
|
}
|
|
|
|
async fn remove_container(&self, name: &str) -> Result<()> {
|
|
self.client.remove_container(name).await
|
|
}
|
|
|
|
async fn get_container_status(&self, name: &str) -> Result<ContainerStatus> {
|
|
self.client.get_container_status(name).await
|
|
}
|
|
|
|
async fn get_container_logs(&self, name: &str, lines: u32) -> Result<Vec<String>> {
|
|
self.client.get_container_logs(name, lines).await
|
|
}
|
|
|
|
async fn list_containers(&self) -> Result<Vec<ContainerStatus>> {
|
|
self.client.list_containers().await
|
|
}
|
|
}
|
|
|
|
pub struct DockerRuntime {
|
|
_user: String,
|
|
}
|
|
|
|
impl DockerRuntime {
|
|
pub fn new(user: String) -> Self {
|
|
Self { _user: user }
|
|
}
|
|
|
|
fn docker_async(&self) -> TokioCommand {
|
|
let mut cmd = TokioCommand::new("docker");
|
|
// Use actual HOME environment variable instead of hardcoded /home
|
|
if let Ok(home) = std::env::var("HOME") {
|
|
cmd.env("HOME", home);
|
|
}
|
|
cmd
|
|
}
|
|
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ContainerRuntime for DockerRuntime {
|
|
async fn pull_image(&self, image: &str, _signature: Option<&str>) -> Result<()> {
|
|
let mut cmd = self.docker_async();
|
|
cmd.arg("pull").arg(image);
|
|
|
|
let output = cmd
|
|
.output()
|
|
.await
|
|
.context("Failed to execute docker pull")?;
|
|
|
|
if !output.status.success() {
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
return Err(anyhow::anyhow!("Failed to pull image: {}", stderr));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn create_container(
|
|
&self,
|
|
manifest: &AppManifest,
|
|
name: &str,
|
|
port_offset: u16,
|
|
) -> Result<String> {
|
|
let mut cmd = self.docker_async();
|
|
cmd.arg("create");
|
|
|
|
cmd.arg("--name").arg(name);
|
|
|
|
if manifest.app.security.readonly_root {
|
|
cmd.arg("--read-only");
|
|
}
|
|
|
|
match manifest.app.security.network_policy.as_str() {
|
|
"host" => {
|
|
cmd.arg("--network").arg("host");
|
|
}
|
|
"isolated" => {
|
|
// Docker uses bridge network by default
|
|
}
|
|
_ => {
|
|
cmd.arg("--network").arg(&manifest.app.security.network_policy);
|
|
}
|
|
}
|
|
|
|
// Port mappings with offset
|
|
for port in &manifest.app.ports {
|
|
let host_port = port.host + port_offset;
|
|
cmd.arg("-p").arg(format!("{}:{}", host_port, port.container));
|
|
}
|
|
|
|
// Volumes
|
|
for volume in &manifest.app.volumes {
|
|
let mut mount = format!("{}:{}", volume.source, volume.target);
|
|
if !volume.options.is_empty() {
|
|
mount.push_str(&format!(":{}", volume.options.join(",")));
|
|
}
|
|
cmd.arg("-v").arg(mount);
|
|
}
|
|
|
|
// Devices
|
|
for device in &manifest.app.devices {
|
|
cmd.arg("--device").arg(device);
|
|
}
|
|
|
|
// Environment variables
|
|
for env in &manifest.app.environment {
|
|
cmd.arg("-e").arg(env);
|
|
}
|
|
|
|
// Resource limits
|
|
if let Some(cpu) = manifest.app.resources.cpu_limit {
|
|
cmd.arg("--cpus").arg(cpu.to_string());
|
|
}
|
|
|
|
if let Some(memory) = &manifest.app.resources.memory_limit {
|
|
cmd.arg("--memory").arg(memory);
|
|
}
|
|
|
|
// Capabilities
|
|
cmd.arg("--cap-drop").arg("ALL");
|
|
for cap in &manifest.app.security.capabilities {
|
|
cmd.arg("--cap-add").arg(cap);
|
|
}
|
|
|
|
cmd.arg(&manifest.app.container.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));
|
|
}
|
|
|
|
let container_id = String::from_utf8_lossy(&output.stdout)
|
|
.trim()
|
|
.to_string();
|
|
|
|
Ok(container_id)
|
|
}
|
|
|
|
async fn start_container(&self, name: &str) -> Result<()> {
|
|
let mut cmd = self.docker_async();
|
|
cmd.arg("start").arg(name);
|
|
|
|
let output = cmd
|
|
.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(())
|
|
}
|
|
|
|
async fn stop_container(&self, name: &str) -> Result<()> {
|
|
let mut cmd = self.docker_async();
|
|
cmd.arg("stop").arg(name);
|
|
|
|
let output = cmd
|
|
.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(())
|
|
}
|
|
|
|
async fn remove_container(&self, name: &str) -> Result<()> {
|
|
let mut cmd = self.docker_async();
|
|
cmd.arg("rm").arg("-f").arg(name);
|
|
|
|
let output = cmd
|
|
.output()
|
|
.await
|
|
.context("Failed to remove container")?;
|
|
|
|
if !output.status.success() {
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
return Err(anyhow::anyhow!("Failed to remove container: {}", stderr));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_container_status(&self, name: &str) -> Result<ContainerStatus> {
|
|
let mut cmd = self.docker_async();
|
|
cmd.arg("inspect")
|
|
.arg("--format")
|
|
.arg("{{.Id}}|{{.Name}}|{{.State.Status}}|{{.Config.Image}}|{{.Created}}|{{.NetworkSettings.Ports}}")
|
|
.arg(name);
|
|
|
|
let output = cmd
|
|
.output()
|
|
.await
|
|
.context("Failed to inspect container")?;
|
|
|
|
if !output.status.success() {
|
|
return Err(anyhow::anyhow!("Container not found: {}", name));
|
|
}
|
|
|
|
let info = String::from_utf8_lossy(&output.stdout);
|
|
let parts: Vec<&str> = info.trim().split('|').collect();
|
|
|
|
if parts.len() < 5 {
|
|
return Err(anyhow::anyhow!("Invalid container inspect output"));
|
|
}
|
|
|
|
Ok(ContainerStatus {
|
|
id: parts[0].to_string(),
|
|
name: parts[1].to_string(),
|
|
state: crate::podman_client::ContainerState::from(parts[2]),
|
|
image: parts[3].to_string(),
|
|
created: parts[4].to_string(),
|
|
ports: vec![],
|
|
lan_address: None,
|
|
})
|
|
}
|
|
|
|
async fn get_container_logs(&self, name: &str, lines: u32) -> Result<Vec<String>> {
|
|
let mut cmd = self.docker_async();
|
|
cmd.arg("logs")
|
|
.arg("--tail")
|
|
.arg(lines.to_string())
|
|
.arg(name);
|
|
|
|
let output = cmd
|
|
.output()
|
|
.await
|
|
.context("Failed to get container logs")?;
|
|
|
|
if !output.status.success() {
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
return Err(anyhow::anyhow!("Failed to get logs: {}", stderr));
|
|
}
|
|
|
|
let logs = String::from_utf8_lossy(&output.stdout);
|
|
Ok(logs.lines().map(|s| s.to_string()).collect())
|
|
}
|
|
|
|
async fn list_containers(&self) -> Result<Vec<ContainerStatus>> {
|
|
let mut cmd = self.docker_async();
|
|
cmd.arg("ps")
|
|
.arg("-a")
|
|
.arg("--format")
|
|
.arg("json");
|
|
|
|
let output = cmd
|
|
.output()
|
|
.await
|
|
.context("Failed to list containers")?;
|
|
|
|
if !output.status.success() {
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
return Err(anyhow::anyhow!("Failed to list containers: {}", stderr));
|
|
}
|
|
|
|
let json = String::from_utf8_lossy(&output.stdout);
|
|
let mut result = Vec::new();
|
|
|
|
// Docker returns NDJSON (newline-delimited JSON), not a JSON array
|
|
for line in json.lines() {
|
|
if line.trim().is_empty() {
|
|
continue;
|
|
}
|
|
|
|
let container: serde_json::Value = serde_json::from_str(line)
|
|
.context(format!("Failed to parse container JSON: {}", line))?;
|
|
|
|
// Extract ports from JSON
|
|
let ports_value = &container["Ports"];
|
|
let ports_str = ports_value.as_str().unwrap_or("");
|
|
let ports: Vec<String> = if !ports_str.is_empty() {
|
|
ports_str.split(", ").map(|s| s.to_string()).collect()
|
|
} else {
|
|
vec![]
|
|
};
|
|
|
|
result.push(ContainerStatus {
|
|
id: container["ID"].as_str().unwrap_or("").to_string(),
|
|
name: container["Names"].as_str().unwrap_or("").to_string(),
|
|
state: ContainerState::from(
|
|
container["State"].as_str().unwrap_or("unknown")
|
|
),
|
|
image: container["Image"].as_str().unwrap_or("").to_string(),
|
|
created: container["CreatedAt"].as_str().unwrap_or("").to_string(),
|
|
ports,
|
|
lan_address: None,
|
|
});
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
}
|
|
|
|
pub struct AutoRuntime {
|
|
runtime: Box<dyn ContainerRuntime>,
|
|
}
|
|
|
|
impl AutoRuntime {
|
|
pub async fn new(user: String) -> Result<Self> {
|
|
// Try Podman first
|
|
if Self::check_podman_available() {
|
|
Ok(Self {
|
|
runtime: Box::new(PodmanRuntime::new(user)),
|
|
})
|
|
} else if Self::check_docker_available() {
|
|
Ok(Self {
|
|
runtime: Box::new(DockerRuntime::new(user)),
|
|
})
|
|
} else {
|
|
Err(anyhow::anyhow!(
|
|
"Neither Podman nor Docker is available"
|
|
))
|
|
}
|
|
}
|
|
|
|
fn check_podman_available() -> bool {
|
|
Command::new("podman")
|
|
.arg("--version")
|
|
.output()
|
|
.is_ok()
|
|
}
|
|
|
|
fn check_docker_available() -> bool {
|
|
Command::new("docker")
|
|
.arg("--version")
|
|
.output()
|
|
.is_ok()
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ContainerRuntime for AutoRuntime {
|
|
async fn pull_image(&self, image: &str, signature: Option<&str>) -> Result<()> {
|
|
self.runtime.pull_image(image, signature).await
|
|
}
|
|
|
|
async fn create_container(
|
|
&self,
|
|
manifest: &AppManifest,
|
|
name: &str,
|
|
port_offset: u16,
|
|
) -> Result<String> {
|
|
self.runtime.create_container(manifest, name, port_offset).await
|
|
}
|
|
|
|
async fn start_container(&self, name: &str) -> Result<()> {
|
|
self.runtime.start_container(name).await
|
|
}
|
|
|
|
async fn stop_container(&self, name: &str) -> Result<()> {
|
|
self.runtime.stop_container(name).await
|
|
}
|
|
|
|
async fn remove_container(&self, name: &str) -> Result<()> {
|
|
self.runtime.remove_container(name).await
|
|
}
|
|
|
|
async fn get_container_status(&self, name: &str) -> Result<ContainerStatus> {
|
|
self.runtime.get_container_status(name).await
|
|
}
|
|
|
|
async fn get_container_logs(&self, name: &str, lines: u32) -> Result<Vec<String>> {
|
|
self.runtime.get_container_logs(name, lines).await
|
|
}
|
|
|
|
async fn list_containers(&self) -> Result<Vec<ContainerStatus>> {
|
|
self.runtime.list_containers().await
|
|
}
|
|
}
|
|
|
|
// Runtime factory functions will be provided by the archipelago crate
|
|
// that imports this library and has access to Config
|