mid coding commit
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde_json::{json, Value};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BitcoinSimulationMode {
|
||||
Mock,
|
||||
Testnet,
|
||||
Mainnet,
|
||||
None,
|
||||
}
|
||||
|
||||
pub struct BitcoinSimulator {
|
||||
mode: BitcoinSimulationMode,
|
||||
rpc_url: Option<String>,
|
||||
mock_blockchain_info: Arc<RwLock<Value>>,
|
||||
}
|
||||
|
||||
impl BitcoinSimulator {
|
||||
pub fn new(mode: BitcoinSimulationMode) -> Self {
|
||||
let mock_blockchain_info = json!({
|
||||
"chain": "main",
|
||||
"blocks": 800000,
|
||||
"headers": 800000,
|
||||
"bestblockhash": "0000000000000000000123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
"difficulty": 50000000000.0,
|
||||
"mediantime": 1700000000,
|
||||
"verificationprogress": 1.0,
|
||||
"initialblockdownload": false,
|
||||
"chainwork": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"size_on_disk": 500000000000i64,
|
||||
"pruned": false,
|
||||
"softforks": {},
|
||||
"warnings": ""
|
||||
});
|
||||
|
||||
let rpc_url = match mode {
|
||||
BitcoinSimulationMode::Mock => None,
|
||||
BitcoinSimulationMode::Testnet => Some("http://localhost:18332".to_string()),
|
||||
BitcoinSimulationMode::Mainnet => Some("http://localhost:8332".to_string()),
|
||||
BitcoinSimulationMode::None => None,
|
||||
};
|
||||
|
||||
Self {
|
||||
mode,
|
||||
rpc_url,
|
||||
mock_blockchain_info: Arc::new(RwLock::new(mock_blockchain_info)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_bitcoin_available(&self) -> bool {
|
||||
match self.mode {
|
||||
BitcoinSimulationMode::Mock => true,
|
||||
BitcoinSimulationMode::Testnet | BitcoinSimulationMode::Mainnet => {
|
||||
// In real mode, we'd check if the container is running
|
||||
// For now, assume it's available if we have an RPC URL
|
||||
self.rpc_url.is_some()
|
||||
}
|
||||
BitcoinSimulationMode::None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_bitcoin_rpc_url(&self) -> Option<String> {
|
||||
self.rpc_url.clone()
|
||||
}
|
||||
|
||||
pub async fn simulate_rpc_call(&self, method: &str, params: &[Value]) -> Result<Value> {
|
||||
match self.mode {
|
||||
BitcoinSimulationMode::Mock => {
|
||||
self.mock_rpc_call(method, params).await
|
||||
}
|
||||
BitcoinSimulationMode::Testnet | BitcoinSimulationMode::Mainnet => {
|
||||
// Make actual RPC call to Bitcoin node
|
||||
self.real_rpc_call(method, params).await
|
||||
}
|
||||
BitcoinSimulationMode::None => {
|
||||
Err(anyhow::anyhow!("Bitcoin simulation is disabled"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn mock_rpc_call(&self, method: &str, _params: &[Value]) -> Result<Value> {
|
||||
match method {
|
||||
"getblockchaininfo" => {
|
||||
let info = self.mock_blockchain_info.read().await;
|
||||
Ok(info.clone())
|
||||
}
|
||||
"getnetworkinfo" => {
|
||||
Ok(json!({
|
||||
"version": 260000,
|
||||
"subversion": "/Bitcoin Core:26.0.0/",
|
||||
"protocolversion": 70016,
|
||||
"localservices": "000000000000040d",
|
||||
"localservicesnames": ["NETWORK", "WITNESS", "NETWORK_LIMITED"],
|
||||
"connections": 8,
|
||||
"connections_in": 4,
|
||||
"connections_out": 4,
|
||||
"networkactive": true,
|
||||
"networks": [],
|
||||
"relayfee": 0.00001000,
|
||||
"incrementalfee": 0.00001000,
|
||||
"localaddresses": [],
|
||||
"warnings": ""
|
||||
}))
|
||||
}
|
||||
"getwalletinfo" => {
|
||||
Ok(json!({
|
||||
"walletname": "wallet.dat",
|
||||
"walletversion": 169900,
|
||||
"balance": 0.0,
|
||||
"unconfirmed_balance": 0.0,
|
||||
"immature_balance": 0.0,
|
||||
"txcount": 0,
|
||||
"keypoololdest": 1700000000,
|
||||
"keypoolsize": 1000,
|
||||
"keypoolsize_hd_internal": 1000,
|
||||
"paytxfee": 0.00000000,
|
||||
"hdseedid": "0000000000000000000000000000000000000000",
|
||||
"private_keys_enabled": true,
|
||||
"avoid_reuse": false,
|
||||
"scanning": false
|
||||
}))
|
||||
}
|
||||
"getblockcount" => {
|
||||
Ok(json!(800000))
|
||||
}
|
||||
"getblockhash" => {
|
||||
Ok(json!("0000000000000000000123456789abcdef0123456789abcdef0123456789abcdef"))
|
||||
}
|
||||
"getmempoolinfo" => {
|
||||
Ok(json!({
|
||||
"loaded": true,
|
||||
"size": 100,
|
||||
"bytes": 100000,
|
||||
"usage": 200000,
|
||||
"total_fee": 0.00001000,
|
||||
"maxmempool": 300000000,
|
||||
"mempoolminfee": 0.00001000,
|
||||
"minrelaytxfee": 0.00001000
|
||||
}))
|
||||
}
|
||||
"getpeerinfo" => {
|
||||
Ok(json!([]))
|
||||
}
|
||||
"getrawmempool" => {
|
||||
Ok(json!([]))
|
||||
}
|
||||
"estimatesmartfee" => {
|
||||
Ok(json!({
|
||||
"feerate": 0.00001000,
|
||||
"blocks": 6
|
||||
}))
|
||||
}
|
||||
_ => {
|
||||
// Default response for unknown methods
|
||||
Ok(json!(null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn real_rpc_call(&self, method: &str, params: &[Value]) -> Result<Value> {
|
||||
let url = self.rpc_url.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("No RPC URL configured"))?;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let request_body = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": method,
|
||||
"params": params
|
||||
});
|
||||
|
||||
// TODO: Get RPC credentials from config/secrets
|
||||
let response = client
|
||||
.post(url)
|
||||
.json(&request_body)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send RPC request")?;
|
||||
|
||||
let response_json: Value = response
|
||||
.json::<Value>()
|
||||
.await
|
||||
.context("Failed to parse RPC response")?;
|
||||
|
||||
if let Some(error) = response_json.get("error") {
|
||||
return Err(anyhow::anyhow!("Bitcoin RPC error: {}", error));
|
||||
}
|
||||
|
||||
Ok(response_json.get("result")
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null))
|
||||
}
|
||||
|
||||
pub fn mode(&self) -> &BitcoinSimulationMode {
|
||||
&self.mode
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for BitcoinSimulationMode {
|
||||
fn from(s: &str) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"mock" => BitcoinSimulationMode::Mock,
|
||||
"testnet" => BitcoinSimulationMode::Testnet,
|
||||
"mainnet" => BitcoinSimulationMode::Mainnet,
|
||||
"none" | _ => BitcoinSimulationMode::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_bitcoin_available() {
|
||||
let simulator = BitcoinSimulator::new(BitcoinSimulationMode::Mock);
|
||||
assert!(simulator.is_bitcoin_available());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_getblockchaininfo() {
|
||||
let simulator = BitcoinSimulator::new(BitcoinSimulationMode::Mock);
|
||||
let result = simulator.simulate_rpc_call("getblockchaininfo", &[]).await.unwrap();
|
||||
assert!(result.get("blocks").is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_none_bitcoin_not_available() {
|
||||
let simulator = BitcoinSimulator::new(BitcoinSimulationMode::None);
|
||||
assert!(!simulator.is_bitcoin_available());
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,14 @@ pub mod manifest;
|
||||
pub mod podman_client;
|
||||
pub mod dependency_resolver;
|
||||
pub mod health_monitor;
|
||||
pub mod runtime;
|
||||
pub mod port_manager;
|
||||
pub mod bitcoin_simulator;
|
||||
|
||||
pub use manifest::{AppManifest, Dependency, ResourceLimits, SecurityPolicy, HealthCheck};
|
||||
pub use podman_client::PodmanClient;
|
||||
pub use podman_client::{PodmanClient, ContainerStatus, ContainerState};
|
||||
pub use dependency_resolver::DependencyResolver;
|
||||
pub use health_monitor::HealthMonitor;
|
||||
pub use runtime::{ContainerRuntime, PodmanRuntime, DockerRuntime, AutoRuntime};
|
||||
pub use port_manager::{PortManager, PortError};
|
||||
pub use bitcoin_simulator::{BitcoinSimulator, BitcoinSimulationMode};
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum PortError {
|
||||
#[error("Port {0} is already allocated to app {1}")]
|
||||
PortConflict(u16, String),
|
||||
#[error("App {0} has no allocated ports")]
|
||||
NoPortsAllocated(String),
|
||||
}
|
||||
|
||||
pub struct PortManager {
|
||||
allocations: Arc<RwLock<HashMap<String, Vec<u16>>>>,
|
||||
port_to_app: Arc<RwLock<HashMap<u16, String>>>,
|
||||
port_offset: u16,
|
||||
}
|
||||
|
||||
impl PortManager {
|
||||
pub fn new(port_offset: u16) -> Self {
|
||||
Self {
|
||||
allocations: Arc::new(RwLock::new(HashMap::new())),
|
||||
port_to_app: Arc::new(RwLock::new(HashMap::new())),
|
||||
port_offset,
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocate ports for an app, applying the port offset
|
||||
pub fn allocate_ports(&self, app_id: &str, base_ports: &[u16]) -> Result<Vec<u16>, PortError> {
|
||||
let mut allocations = self.allocations.write().unwrap();
|
||||
let mut port_to_app = self.port_to_app.write().unwrap();
|
||||
let mut allocated_ports = Vec::new();
|
||||
|
||||
// Check for conflicts and allocate ports
|
||||
for &base_port in base_ports {
|
||||
let dev_port = base_port + self.port_offset;
|
||||
|
||||
// Check if port is already allocated
|
||||
if let Some(existing_app) = port_to_app.get(&dev_port) {
|
||||
if existing_app != app_id {
|
||||
return Err(PortError::PortConflict(dev_port, existing_app.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
allocated_ports.push(dev_port);
|
||||
port_to_app.insert(dev_port, app_id.to_string());
|
||||
}
|
||||
|
||||
// Store allocation for this app
|
||||
allocations.insert(app_id.to_string(), allocated_ports.clone());
|
||||
|
||||
Ok(allocated_ports)
|
||||
}
|
||||
|
||||
/// Get allocated ports for an app
|
||||
pub fn get_port_mapping(&self, app_id: &str) -> Option<Vec<u16>> {
|
||||
let allocations = self.allocations.read().unwrap();
|
||||
allocations.get(app_id).cloned()
|
||||
}
|
||||
|
||||
/// Get the dev port for a specific base port of an app
|
||||
pub fn get_dev_port(&self, app_id: &str, base_port: u16) -> Option<u16> {
|
||||
self.get_port_mapping(app_id)
|
||||
.and_then(|ports| {
|
||||
// Find the port that corresponds to this base port
|
||||
ports.iter().find(|&&p| p == base_port + self.port_offset).copied()
|
||||
})
|
||||
}
|
||||
|
||||
/// Release all ports allocated to an app
|
||||
pub fn release_ports(&self, app_id: &str) -> Result<(), PortError> {
|
||||
let mut allocations = self.allocations.write().unwrap();
|
||||
let mut port_to_app = self.port_to_app.write().unwrap();
|
||||
|
||||
if let Some(ports) = allocations.remove(app_id) {
|
||||
for port in ports {
|
||||
port_to_app.remove(&port);
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(PortError::NoPortsAllocated(app_id.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a port is available
|
||||
pub fn is_port_available(&self, base_port: u16) -> bool {
|
||||
let dev_port = base_port + self.port_offset;
|
||||
let port_to_app = self.port_to_app.read().unwrap();
|
||||
!port_to_app.contains_key(&dev_port)
|
||||
}
|
||||
|
||||
/// Get all allocated ports
|
||||
pub fn get_all_allocations(&self) -> HashMap<String, Vec<u16>> {
|
||||
let allocations = self.allocations.read().unwrap();
|
||||
allocations.clone()
|
||||
}
|
||||
|
||||
/// Get port offset
|
||||
pub fn port_offset(&self) -> u16 {
|
||||
self.port_offset
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_port_allocation() {
|
||||
let manager = PortManager::new(10000);
|
||||
|
||||
let ports = manager.allocate_ports("app1", &[8332, 8333]).unwrap();
|
||||
assert_eq!(ports, vec![18332, 18333]);
|
||||
|
||||
let mapping = manager.get_port_mapping("app1").unwrap();
|
||||
assert_eq!(mapping, vec![18332, 18333]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_port_conflict() {
|
||||
let manager = PortManager::new(10000);
|
||||
|
||||
manager.allocate_ports("app1", &[8332]).unwrap();
|
||||
|
||||
// Try to allocate the same port to another app
|
||||
let result = manager.allocate_ports("app2", &[8332]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_port_release() {
|
||||
let manager = PortManager::new(10000);
|
||||
|
||||
manager.allocate_ports("app1", &[8332]).unwrap();
|
||||
manager.release_ports("app1").unwrap();
|
||||
|
||||
// Port should now be available
|
||||
assert!(manager.is_port_available(8332));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_dev_port() {
|
||||
let manager = PortManager::new(10000);
|
||||
|
||||
manager.allocate_ports("app1", &[8332, 8333]).unwrap();
|
||||
|
||||
assert_eq!(manager.get_dev_port("app1", 8332), Some(18332));
|
||||
assert_eq!(manager.get_dev_port("app1", 8333), Some(18333));
|
||||
assert_eq!(manager.get_dev_port("app1", 9999), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
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 }
|
||||
}
|
||||
|
||||
fn docker_async(&self) -> TokioCommand {
|
||||
let mut cmd = TokioCommand::new("docker");
|
||||
cmd.env("HOME", format!("/home/{}", self.user));
|
||||
cmd
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn docker_command(&self) -> Command {
|
||||
let mut cmd = Command::new("docker");
|
||||
cmd.env("HOME", format!("/home/{}", self.user));
|
||||
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![],
|
||||
})
|
||||
}
|
||||
|
||||
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 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"].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: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user