Initial commit
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
use crate::manifest::{AppManifest, PortMapping, Volume};
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::process::{Command, Stdio};
|
||||
use thiserror::Error;
|
||||
use tokio::process::Command as TokioCommand;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum PodmanError {
|
||||
#[error("Podman command failed: {0}")]
|
||||
CommandFailed(String),
|
||||
#[error("Container not found: {0}")]
|
||||
NotFound(String),
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContainerStatus {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub state: ContainerState,
|
||||
pub image: String,
|
||||
pub created: String,
|
||||
pub ports: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ContainerState {
|
||||
Created,
|
||||
Running,
|
||||
Stopped,
|
||||
Exited,
|
||||
Paused,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<&str> for ContainerState {
|
||||
fn from(s: &str) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"created" => ContainerState::Created,
|
||||
"running" => ContainerState::Running,
|
||||
"stopped" => ContainerState::Stopped,
|
||||
"exited" => ContainerState::Exited,
|
||||
"paused" => ContainerState::Paused,
|
||||
other => ContainerState::Unknown(other.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PodmanClient {
|
||||
user: String,
|
||||
rootless: bool,
|
||||
}
|
||||
|
||||
impl PodmanClient {
|
||||
pub fn new(user: String) -> Self {
|
||||
Self {
|
||||
user,
|
||||
rootless: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn podman_command(&self) -> Command {
|
||||
let mut cmd = Command::new("podman");
|
||||
if self.rootless {
|
||||
// Run as the specified user
|
||||
cmd.env("HOME", format!("/home/{}", self.user));
|
||||
}
|
||||
cmd
|
||||
}
|
||||
|
||||
fn podman_async(&self) -> TokioCommand {
|
||||
let mut cmd = TokioCommand::new("podman");
|
||||
if self.rootless {
|
||||
cmd.env("HOME", format!("/home/{}", self.user));
|
||||
}
|
||||
cmd
|
||||
}
|
||||
|
||||
pub async fn pull_image(&self, image: &str, signature: Option<&str>) -> Result<()> {
|
||||
let mut cmd = self.podman_async();
|
||||
cmd.arg("pull").arg(image);
|
||||
|
||||
if let Some(sig) = signature {
|
||||
// Verify signature with cosign if provided
|
||||
cmd.arg("--signature-policy").arg("default");
|
||||
// TODO: Implement cosign verification
|
||||
log::warn!("Signature verification not yet implemented: {}", sig);
|
||||
}
|
||||
|
||||
let output = cmd
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to execute podman pull")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow::anyhow!("Failed to pull image: {}", stderr));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_container(
|
||||
&self,
|
||||
manifest: &AppManifest,
|
||||
name: &str,
|
||||
) -> Result<String> {
|
||||
let mut cmd = self.podman_async();
|
||||
cmd.arg("create");
|
||||
|
||||
// Container name
|
||||
cmd.arg("--name").arg(name);
|
||||
|
||||
// Read-only root filesystem
|
||||
if manifest.app.security.readonly_root {
|
||||
cmd.arg("--read-only");
|
||||
}
|
||||
|
||||
// Network policy
|
||||
match manifest.app.security.network_policy.as_str() {
|
||||
"host" => {
|
||||
cmd.arg("--network").arg("host");
|
||||
}
|
||||
"isolated" => {
|
||||
// Create isolated network (default)
|
||||
}
|
||||
_ => {
|
||||
cmd.arg("--network").arg(&manifest.app.security.network_policy);
|
||||
}
|
||||
}
|
||||
|
||||
// Port mappings
|
||||
for port in &manifest.app.ports {
|
||||
cmd.arg("-p").arg(format!("{}:{}", port.host, 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 (drop all, add specified)
|
||||
cmd.arg("--cap-drop").arg("ALL");
|
||||
for cap in &manifest.app.security.capabilities {
|
||||
cmd.arg("--cap-add").arg(cap);
|
||||
}
|
||||
|
||||
// Image
|
||||
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)
|
||||
}
|
||||
|
||||
pub async fn start_container(&self, name: &str) -> Result<()> {
|
||||
let mut cmd = self.podman_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(())
|
||||
}
|
||||
|
||||
pub async fn stop_container(&self, name: &str) -> Result<()> {
|
||||
let mut cmd = self.podman_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(())
|
||||
}
|
||||
|
||||
pub async fn remove_container(&self, name: &str) -> Result<()> {
|
||||
let mut cmd = self.podman_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(())
|
||||
}
|
||||
|
||||
pub async fn get_container_status(&self, name: &str) -> Result<ContainerStatus> {
|
||||
let mut cmd = self.podman_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: ContainerState::from(parts[2]),
|
||||
image: parts[3].to_string(),
|
||||
created: parts[4].to_string(),
|
||||
ports: vec![], // TODO: Parse ports from parts[5]
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_container_logs(&self, name: &str, lines: u32) -> Result<Vec<String>> {
|
||||
let mut cmd = self.podman_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())
|
||||
}
|
||||
|
||||
pub async fn list_containers(&self) -> Result<Vec<ContainerStatus>> {
|
||||
let mut cmd = self.podman_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 containers: Vec<serde_json::Value> = serde_json::from_str(&json)
|
||||
.context("Failed to parse container list")?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for container in containers {
|
||||
result.push(ContainerStatus {
|
||||
id: container["Id"].as_str().unwrap_or("").to_string(),
|
||||
name: container["Names"][0].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["Created"].as_str().unwrap_or("").to_string(),
|
||||
ports: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user