Initial commit

This commit is contained in:
zazawowow
2026-01-24 22:01:51 +00:00
commit 64cc3bc7fb
56 changed files with 4584 additions and 0 deletions

24
core/container/Cargo.toml Normal file
View File

@@ -0,0 +1,24 @@
[package]
name = "archipelago-container"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9"
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
thiserror = "1.0"
anyhow = "1.0"
async-trait = "0.1"
futures = "0.3"
indexmap = { version = "2.0", features = ["serde"] }
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.0", features = ["v4"] }
log = "0.4"
tracing = "0.1"
[lib]
name = "archipelago_container"
path = "src/lib.rs"

View File

@@ -0,0 +1,255 @@
use crate::manifest::{AppManifest, Dependency};
use indexmap::IndexMap;
use std::collections::{HashMap, HashSet};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum DependencyError {
#[error("Circular dependency detected: {0}")]
CircularDependency(String),
#[error("Missing dependency: {0}")]
MissingDependency(String),
#[error("Version conflict: {0}")]
VersionConflict(String),
}
pub struct DependencyResolver {
manifests: IndexMap<String, AppManifest>,
}
impl DependencyResolver {
pub fn new() -> Self {
Self {
manifests: IndexMap::new(),
}
}
pub fn add_manifest(&mut self, manifest: AppManifest) {
self.manifests.insert(manifest.app.id.clone(), manifest);
}
pub fn resolve_dependencies(&self, app_id: &str) -> Result<Vec<String>, DependencyError> {
let mut visited = HashSet::new();
let mut visiting = HashSet::new();
let mut result = Vec::new();
self.resolve_recursive(app_id, &mut visited, &mut visiting, &mut result)?;
// Reverse to get installation order (dependencies first)
result.reverse();
Ok(result)
}
fn resolve_recursive(
&self,
app_id: &str,
visited: &mut HashSet<String>,
visiting: &mut HashSet<String>,
result: &mut Vec<String>,
) -> Result<(), DependencyError> {
if visited.contains(app_id) {
return Ok(());
}
if visiting.contains(app_id) {
return Err(DependencyError::CircularDependency(
format!("Circular dependency detected involving: {}", app_id)
));
}
visiting.insert(app_id.to_string());
let manifest = self.manifests.get(app_id)
.ok_or_else(|| DependencyError::MissingDependency(
format!("App not found: {}", app_id)
))?;
// Resolve all dependencies first
for dep in &manifest.app.dependencies {
match dep {
Dependency::App { app_id: dep_id, version: _ } => {
self.resolve_recursive(dep_id, visited, visiting, result)?;
}
Dependency::Storage { storage: _ } => {
// Storage dependencies are checked but don't require other apps
}
Dependency::Simple(dep_id) => {
self.resolve_recursive(dep_id, visited, visiting, result)?;
}
}
}
visiting.remove(app_id);
visited.insert(app_id.to_string());
if !result.contains(&app_id.to_string()) {
result.push(app_id.to_string());
}
Ok(())
}
pub fn check_conflicts(&self, app_id: &str) -> Result<(), DependencyError> {
let manifest = self.manifests.get(app_id)
.ok_or_else(|| DependencyError::MissingDependency(
format!("App not found: {}", app_id)
))?;
// Check for port conflicts
let mut port_usage: HashMap<u16, String> = HashMap::new();
for (id, m) in &self.manifests {
if id == app_id {
continue;
}
for port in &m.app.ports {
if let Some(existing) = port_usage.get(&port.host) {
return Err(DependencyError::VersionConflict(
format!("Port {} already used by {}", port.host, existing)
));
}
port_usage.insert(port.host, id.clone());
}
}
// Check for new app's ports
for port in &manifest.app.ports {
if let Some(existing) = port_usage.get(&port.host) {
return Err(DependencyError::VersionConflict(
format!("Port {} already used by {}", port.host, existing)
));
}
}
Ok(())
}
pub fn calculate_resources(&self, app_ids: &[String]) -> ResourceRequirements {
let mut total = ResourceRequirements {
cpu: 0,
memory_mb: 0,
disk_gb: 0,
};
for app_id in app_ids {
if let Some(manifest) = self.manifests.get(app_id) {
if let Some(cpu) = manifest.app.resources.cpu_limit {
total.cpu += cpu;
}
if let Some(memory) = &manifest.app.resources.memory_limit {
// Parse memory string (e.g., "1Gi", "512Mi")
if let Ok(mb) = parse_memory(memory) {
total.memory_mb += mb;
}
}
if let Some(disk) = &manifest.app.resources.disk_limit {
// Parse disk string (e.g., "10Gi", "500Mi")
if let Ok(gb) = parse_disk(disk) {
total.disk_gb += gb;
}
}
}
}
total
}
}
fn parse_memory(s: &str) -> Result<u32, ()> {
let s = s.trim().to_lowercase();
if s.ends_with("gi") {
let num: f64 = s.trim_end_matches("gi").parse().map_err(|_| ())?;
Ok((num * 1024.0) as u32)
} else if s.ends_with("mi") {
let num: f64 = s.trim_end_matches("mi").parse().map_err(|_| ())?;
Ok(num as u32)
} else {
Err(())
}
}
fn parse_disk(s: &str) -> Result<u32, ()> {
let s = s.trim().to_lowercase();
if s.ends_with("gi") {
let num: f64 = s.trim_end_matches("gi").parse().map_err(|_| ())?;
Ok(num as u32)
} else if s.ends_with("ti") {
let num: f64 = s.trim_end_matches("ti").parse().map_err(|_| ())?;
Ok((num * 1024.0) as u32)
} else {
Err(())
}
}
#[derive(Debug, Clone)]
pub struct ResourceRequirements {
pub cpu: u32,
pub memory_mb: u32,
pub disk_gb: u32,
}
impl Default for DependencyResolver {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::{AppManifest, AppDefinition, ContainerConfig};
fn create_test_manifest(id: &str, deps: Vec<Dependency>) -> AppManifest {
AppManifest {
app: AppDefinition {
id: id.to_string(),
name: format!("Test {}", id),
version: "1.0.0".to_string(),
description: None,
container: ContainerConfig {
image: format!("test/{}:latest", id),
image_signature: None,
pull_policy: "if-not-present".to_string(),
},
dependencies: deps,
resources: Default::default(),
security: Default::default(),
ports: vec![],
volumes: vec![],
environment: vec![],
health_check: None,
devices: vec![],
extensions: Default::default(),
},
}
}
#[test]
fn test_simple_dependency() {
let mut resolver = DependencyResolver::new();
resolver.add_manifest(create_test_manifest("app1", vec![]));
resolver.add_manifest(create_test_manifest("app2", vec![
Dependency::Simple("app1".to_string())
]));
let deps = resolver.resolve_dependencies("app2").unwrap();
assert_eq!(deps, vec!["app1", "app2"]);
}
#[test]
fn test_circular_dependency() {
let mut resolver = DependencyResolver::new();
resolver.add_manifest(create_test_manifest("app1", vec![
Dependency::Simple("app2".to_string())
]));
resolver.add_manifest(create_test_manifest("app2", vec![
Dependency::Simple("app1".to_string())
]));
let result = resolver.resolve_dependencies("app1");
assert!(result.is_err());
}
}

View File

@@ -0,0 +1,189 @@
use crate::manifest::HealthCheck;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use tokio::time::{interval, sleep};
use tracing::{error, info, warn};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum HealthStatus {
Healthy,
Unhealthy,
Unknown,
Starting,
}
pub struct HealthMonitor {
container_name: String,
health_check: Option<HealthCheck>,
}
impl HealthMonitor {
pub fn new(container_name: String, health_check: Option<HealthCheck>) -> Self {
Self {
container_name,
health_check,
}
}
pub async fn check_health(&self) -> Result<HealthStatus> {
if let Some(ref check) = self.health_check {
match check.check_type.as_str() {
"http" => self.check_http_health(check).await,
"exec" => self.check_exec_health(check).await,
_ => {
warn!("Unknown health check type: {}", check.check_type);
Ok(HealthStatus::Unknown)
}
}
} else {
// No health check defined, assume healthy if container is running
Ok(HealthStatus::Unknown)
}
}
async fn check_http_health(&self, check: &HealthCheck) -> Result<HealthStatus> {
let endpoint = check.endpoint.as_ref()
.ok_or_else(|| anyhow::anyhow!("HTTP health check missing endpoint"))?;
let url = if let Some(path) = &check.path {
format!("{}{}", endpoint, path)
} else {
endpoint.clone()
};
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(5))
.build()
.context("Failed to create HTTP client")?;
match client.get(&url).send().await {
Ok(response) => {
if response.status().is_success() {
Ok(HealthStatus::Healthy)
} else {
Ok(HealthStatus::Unhealthy)
}
}
Err(e) => {
warn!("Health check failed for {}: {}", self.container_name, e);
Ok(HealthStatus::Unhealthy)
}
}
}
async fn check_exec_health(&self, check: &HealthCheck) -> Result<HealthStatus> {
// Execute health check command in container
let endpoint = check.endpoint.as_ref()
.ok_or_else(|| anyhow::anyhow!("Exec health check missing endpoint"))?;
use tokio::process::Command;
let output = Command::new("podman")
.arg("exec")
.arg(&self.container_name)
.arg("sh")
.arg("-c")
.arg(endpoint)
.output()
.await
.context("Failed to execute health check")?;
if output.status.success() {
Ok(HealthStatus::Healthy)
} else {
Ok(HealthStatus::Unhealthy)
}
}
pub async fn monitor_health(
&self,
mut shutdown: tokio::sync::broadcast::Receiver<()>,
on_status_change: impl Fn(HealthStatus) + Send + 'static,
) -> Result<()> {
let check = self.health_check.clone();
let interval_duration = if let Some(ref check) = check {
parse_duration(&check.interval).unwrap_or(Duration::from_secs(30))
} else {
Duration::from_secs(30)
};
let mut interval = interval(interval_duration);
let mut consecutive_failures = 0;
let max_failures = check.as_ref()
.map(|c| c.retries)
.unwrap_or(3);
let mut last_status = HealthStatus::Unknown;
loop {
tokio::select! {
_ = interval.tick() => {
match self.check_health().await {
Ok(status) => {
if status != last_status {
info!("Health status changed for {}: {:?} -> {:?}",
self.container_name, last_status, status);
on_status_change(status.clone());
last_status = status.clone();
}
match status {
HealthStatus::Healthy => {
consecutive_failures = 0;
}
HealthStatus::Unhealthy => {
consecutive_failures += 1;
if consecutive_failures >= max_failures {
error!("Container {} is unhealthy after {} failures",
self.container_name, consecutive_failures);
// TODO: Trigger auto-restart or alert
}
}
_ => {}
}
}
Err(e) => {
error!("Health check error for {}: {}", self.container_name, e);
consecutive_failures += 1;
}
}
}
_ = shutdown.recv() => {
info!("Health monitoring stopped for {}", self.container_name);
break;
}
}
}
Ok(())
}
}
fn parse_duration(s: &str) -> Option<Duration> {
let s = s.trim().to_lowercase();
if s.ends_with('s') {
let secs: u64 = s.trim_end_matches('s').parse().ok()?;
Some(Duration::from_secs(secs))
} else if s.ends_with('m') {
let mins: u64 = s.trim_end_matches('m').parse().ok()?;
Some(Duration::from_secs(mins * 60))
} else if s.ends_with('h') {
let hours: u64 = s.trim_end_matches('h').parse().ok()?;
Some(Duration::from_secs(hours * 3600))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_duration() {
assert_eq!(parse_duration("30s"), Some(Duration::from_secs(30)));
assert_eq!(parse_duration("5m"), Some(Duration::from_secs(300)));
assert_eq!(parse_duration("1h"), Some(Duration::from_secs(3600)));
}
}

View File

@@ -0,0 +1,9 @@
pub mod manifest;
pub mod podman_client;
pub mod dependency_resolver;
pub mod health_monitor;
pub use manifest::{AppManifest, Dependency, ResourceLimits, SecurityPolicy, HealthCheck};
pub use podman_client::PodmanClient;
pub use dependency_resolver::DependencyResolver;
pub use health_monitor::HealthMonitor;

View File

@@ -0,0 +1,228 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ManifestError {
#[error("Invalid manifest: {0}")]
Invalid(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("YAML parse error: {0}")]
Yaml(#[from] serde_yaml::Error),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppManifest {
pub app: AppDefinition,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppDefinition {
pub id: String,
pub name: String,
pub version: String,
pub description: Option<String>,
#[serde(default)]
pub container: ContainerConfig,
#[serde(default)]
pub dependencies: Vec<Dependency>,
#[serde(default)]
pub resources: ResourceLimits,
#[serde(default)]
pub security: SecurityPolicy,
#[serde(default)]
pub ports: Vec<PortMapping>,
#[serde(default)]
pub volumes: Vec<Volume>,
#[serde(default)]
pub environment: Vec<String>,
#[serde(default)]
pub health_check: Option<HealthCheck>,
#[serde(default)]
pub devices: Vec<String>,
#[serde(flatten)]
pub extensions: HashMap<String, serde_yaml::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ContainerConfig {
pub image: String,
#[serde(default)]
pub image_signature: Option<String>,
#[serde(default = "default_pull_policy")]
pub pull_policy: String,
}
fn default_pull_policy() -> String {
"if-not-present".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Dependency {
Storage { storage: String },
App { app_id: String, version: Option<String> },
Simple(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ResourceLimits {
#[serde(default)]
pub cpu_limit: Option<u32>,
#[serde(default)]
pub memory_limit: Option<String>,
#[serde(default)]
pub disk_limit: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SecurityPolicy {
#[serde(default)]
pub capabilities: Vec<String>,
#[serde(default = "default_true")]
pub readonly_root: bool,
#[serde(default = "default_network_policy")]
pub network_policy: String,
#[serde(default)]
pub apparmor_profile: Option<String>,
}
fn default_true() -> bool {
true
}
fn default_network_policy() -> String {
"isolated".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PortMapping {
pub host: u16,
pub container: u16,
#[serde(default)]
pub protocol: String,
}
impl From<(u16, u16)> for PortMapping {
fn from((host, container): (u16, u16)) -> Self {
PortMapping {
host,
container,
protocol: "tcp".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Volume {
#[serde(rename = "type")]
pub volume_type: String,
pub source: String,
pub target: String,
#[serde(default)]
pub options: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthCheck {
#[serde(rename = "type")]
pub check_type: String,
pub endpoint: Option<String>,
pub path: Option<String>,
#[serde(default = "default_interval")]
pub interval: String,
#[serde(default = "default_timeout")]
pub timeout: String,
#[serde(default = "default_retries")]
pub retries: u32,
}
fn default_interval() -> String {
"30s".to_string()
}
fn default_timeout() -> String {
"5s".to_string()
}
fn default_retries() -> u32 {
3
}
impl AppManifest {
pub fn from_file(path: &std::path::Path) -> Result<Self, ManifestError> {
let content = std::fs::read_to_string(path)?;
Self::from_str(&content)
}
pub fn from_str(content: &str) -> Result<Self, ManifestError> {
let manifest: AppManifest = serde_yaml::from_str(content)?;
manifest.validate()?;
Ok(manifest)
}
pub fn validate(&self) -> Result<(), ManifestError> {
if self.app.id.is_empty() {
return Err(ManifestError::Invalid("app.id cannot be empty".to_string()));
}
if self.app.container.image.is_empty() {
return Err(ManifestError::Invalid("container.image cannot be empty".to_string()));
}
// Validate version format (semantic versioning)
if !self.app.version.chars().any(|c| c.is_ascii_digit()) {
return Err(ManifestError::Invalid("app.version must contain at least one digit".to_string()));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_manifest_parse() {
let yaml = r#"
app:
id: test-app
name: Test App
version: 1.0.0
container:
image: test/image:latest
"#;
let manifest = AppManifest::from_str(yaml).unwrap();
assert_eq!(manifest.app.id, "test-app");
assert_eq!(manifest.app.name, "Test App");
assert_eq!(manifest.app.version, "1.0.0");
}
#[test]
fn test_manifest_validation() {
let yaml = r#"
app:
id: ""
name: Test
version: 1.0.0
container:
image: test/image:latest
"#;
let result = AppManifest::from_str(yaml);
assert!(result.is_err());
}
}

View File

@@ -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)
}
}