chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job

The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:

- Applies rustfmt across the tree (the bulk of the diff — untouched
  since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
    container/bitcoin_simulator.rs wildcard-in-or-pattern
    container/manifest.rs from_str rename to parse (reserved name)
    container/podman_client.rs .get(0) -> .first()
    container/runtime.rs manual += collapse
    archipelago/src/constants.rs doc-comment → module-doc
    api/rpc/package/install.rs stray /// comment above a non-item
    container/docker_packages.rs redundant field init
    streaming/advertisement.rs missing Metric import in tests
    tests/orchestration_tests.rs `vec!` in non-Vec contexts
    mesh/listener/dispatch.rs unused store_plain_message import
    api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
  stylistic lints (too_many_arguments, type_complexity, doc indent,
  enum variant prefix, wildcard-in-or, assertions-on-constants,
  drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
  of places with no correctness payoff and have been churning every
  toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
  are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
  rollback compatibility, vpn::get_nostr_vpn_status is surface-area
  for a not-yet-landed RPC.

cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-18 17:23:46 -04:00
parent 3a52c766ac
commit b614c5c694
173 changed files with 6658 additions and 3433 deletions

View File

@@ -67,16 +67,12 @@ impl BitcoinSimulator {
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::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"))
}
BitcoinSimulationMode::None => Err(anyhow::anyhow!("Bitcoin simulation is disabled")),
}
}
@@ -86,72 +82,58 @@ impl BitcoinSimulator {
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
}))
}
"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))
@@ -160,7 +142,9 @@ impl BitcoinSimulator {
}
async fn real_rpc_call(&self, method: &str, params: &[Value]) -> Result<Value> {
let url = self.rpc_url.as_ref()
let url = self
.rpc_url
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No RPC URL configured"))?;
let client = reqwest::Client::new();
@@ -188,9 +172,7 @@ impl BitcoinSimulator {
return Err(anyhow::anyhow!("Bitcoin RPC error: {}", error));
}
Ok(response_json.get("result")
.cloned()
.unwrap_or(Value::Null))
Ok(response_json.get("result").cloned().unwrap_or(Value::Null))
}
pub fn mode(&self) -> &BitcoinSimulationMode {
@@ -204,7 +186,7 @@ impl From<&str> for BitcoinSimulationMode {
"mock" => BitcoinSimulationMode::Mock,
"testnet" => BitcoinSimulationMode::Testnet,
"mainnet" => BitcoinSimulationMode::Mainnet,
"none" | _ => BitcoinSimulationMode::None,
_ => BitcoinSimulationMode::None,
}
}
}
@@ -222,7 +204,10 @@ mod tests {
#[tokio::test]
async fn test_mock_getblockchaininfo() {
let simulator = BitcoinSimulator::new(BitcoinSimulationMode::Mock);
let result = simulator.simulate_rpc_call("getblockchaininfo", &[]).await.unwrap();
let result = simulator
.simulate_rpc_call("getblockchaininfo", &[])
.await
.unwrap();
assert!(result.get("blocks").is_some());
}

View File

@@ -23,22 +23,22 @@ impl DependencyResolver {
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)?;
// Result is already in installation order (dependencies first)
Ok(result)
}
fn resolve_recursive(
&self,
app_id: &str,
@@ -49,24 +49,27 @@ impl DependencyResolver {
if visited.contains(app_id) {
return Ok(());
}
if visiting.contains(app_id) {
return Err(DependencyError::CircularDependency(
format!("Circular dependency detected involving: {}", 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)
))?;
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: _ } => {
Dependency::App {
app_id: dep_id,
version: _,
} => {
self.resolve_recursive(dep_id, visited, visiting, result)?;
}
Dependency::Storage { storage: _ } => {
@@ -77,73 +80,74 @@ impl DependencyResolver {
}
}
}
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)
))?;
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)
));
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)
));
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) {
@@ -152,7 +156,7 @@ impl DependencyResolver {
}
}
}
total
}
}
@@ -199,8 +203,8 @@ impl Default for DependencyResolver {
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::{AppManifest, AppDefinition, ContainerConfig};
use crate::manifest::{AppDefinition, AppManifest, ContainerConfig};
fn create_test_manifest(id: &str, deps: Vec<Dependency>) -> AppManifest {
AppManifest {
app: AppDefinition {
@@ -225,29 +229,32 @@ mod tests {
},
}
}
#[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())
]));
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())
]));
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

@@ -25,7 +25,7 @@ impl HealthMonitor {
health_check,
}
}
pub async fn check_health(&self) -> Result<HealthStatus> {
if let Some(ref check) = self.health_check {
match check.check_type.as_str() {
@@ -41,22 +41,24 @@ impl HealthMonitor {
Ok(HealthStatus::Unknown)
}
}
async fn check_http_health(&self, check: &HealthCheck) -> Result<HealthStatus> {
let endpoint = check.endpoint.as_ref()
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() {
@@ -71,14 +73,16 @@ impl HealthMonitor {
}
}
}
async fn check_exec_health(&self, check: &HealthCheck) -> Result<HealthStatus> {
// Execute health check command in container
let endpoint = check.endpoint.as_ref()
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)
@@ -88,14 +92,14 @@ impl HealthMonitor {
.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<()>,
@@ -107,27 +111,25 @@ impl HealthMonitor {
} 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 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 {}: {:?} -> {:?}",
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;
@@ -160,7 +162,7 @@ impl HealthMonitor {
}
}
}
Ok(())
}
}
@@ -184,7 +186,7 @@ fn parse_duration(s: &str) -> Option<Duration> {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_duration() {
assert_eq!(parse_duration("30s"), Some(Duration::from_secs(30)));

View File

@@ -1,15 +1,15 @@
pub mod manifest;
pub mod podman_client;
pub mod bitcoin_simulator;
pub mod dependency_resolver;
pub mod health_monitor;
pub mod runtime;
pub mod manifest;
pub mod podman_client;
pub mod port_manager;
pub mod bitcoin_simulator;
pub mod runtime;
pub use manifest::{AppManifest, Dependency, ResourceLimits, SecurityPolicy, HealthCheck};
pub use podman_client::{PodmanClient, ContainerStatus, ContainerState};
pub use bitcoin_simulator::{BitcoinSimulationMode, BitcoinSimulator};
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};
pub use manifest::{AppManifest, Dependency, HealthCheck, ResourceLimits, SecurityPolicy};
pub use podman_client::{ContainerState, ContainerStatus, PodmanClient};
pub use port_manager::{PortError, PortManager};
pub use runtime::{AutoRuntime, ContainerRuntime, DockerRuntime, PodmanRuntime};

View File

@@ -23,34 +23,34 @@ pub struct AppDefinition {
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>,
}
@@ -71,8 +71,13 @@ fn default_pull_policy() -> String {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Dependency {
Storage { storage: String },
App { app_id: String, version: Option<String> },
Storage {
storage: String,
},
App {
app_id: String,
version: Option<String>,
},
Simple(String),
}
@@ -163,29 +168,33 @@ fn default_retries() -> u32 {
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)
Self::parse(&content)
}
pub fn from_str(content: &str) -> Result<Self, ManifestError> {
pub fn parse(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()));
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()));
return Err(ManifestError::Invalid(
"app.version must contain at least one digit".to_string(),
));
}
Ok(())
}
}
@@ -193,7 +202,7 @@ impl AppManifest {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_manifest_parse() {
let yaml = r#"
@@ -204,13 +213,13 @@ app:
container:
image: test/image:latest
"#;
let manifest = AppManifest::from_str(yaml).unwrap();
let manifest = AppManifest::parse(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#"
@@ -221,8 +230,8 @@ app:
container:
image: test/image:latest
"#;
let result = AppManifest::from_str(yaml);
let result = AppManifest::parse(yaml);
assert!(result.is_err());
}
}

View File

@@ -158,7 +158,10 @@ impl PodmanClient {
)
.await
.map_err(|_| anyhow::anyhow!("Podman socket connection timed out (30s)"))?
.context(format!("Cannot connect to Podman socket at {}", socket_path.display()))?;
.context(format!(
"Cannot connect to Podman socket at {}",
socket_path.display()
))?;
// Build the hyper client with the unix stream
let (mut sender, conn) = hyper::client::conn::Builder::new()
@@ -193,28 +196,26 @@ impl PodmanClient {
.body(Body::from(body_str))
.context("Failed to build POST request")?
}
"DELETE" => {
Request::builder()
.method("DELETE")
.uri(uri)
.header("Host", "localhost")
.body(Body::empty())
.context("Failed to build DELETE request")?
}
_ => {
Request::builder()
.method("GET")
.uri(uri)
.header("Host", "localhost")
.body(Body::empty())
.context("Failed to build GET request")?
}
"DELETE" => Request::builder()
.method("DELETE")
.uri(uri)
.header("Host", "localhost")
.body(Body::empty())
.context("Failed to build DELETE request")?,
_ => Request::builder()
.method("GET")
.uri(uri)
.header("Host", "localhost")
.body(Body::empty())
.context("Failed to build GET request")?,
};
// Send with timeout
let resp = tokio::time::timeout(timeout, sender.send_request(req))
.await
.map_err(|_| anyhow::anyhow!("Podman API request timed out after {}s", timeout.as_secs()))?
.map_err(|_| {
anyhow::anyhow!("Podman API request timed out after {}s", timeout.as_secs())
})?
.context("Podman API request failed")?;
let status = resp.status();
@@ -228,7 +229,12 @@ impl PodmanClient {
if !status.is_success() {
let error_text = String::from_utf8_lossy(&body_bytes);
return Err(anyhow::anyhow!("Podman API {} {}: {}", status.as_u16(), status.canonical_reason().unwrap_or(""), error_text));
return Err(anyhow::anyhow!(
"Podman API {} {}: {}",
status.as_u16(),
status.canonical_reason().unwrap_or(""),
error_text
));
}
// Some endpoints return empty body on success (start/stop/restart)
@@ -236,13 +242,13 @@ impl PodmanClient {
return Ok(serde_json::json!({"ok": true}));
}
serde_json::from_slice(&body_bytes)
.context("Failed to parse Podman API JSON response")
serde_json::from_slice(&body_bytes).context("Failed to parse Podman API JSON response")
}
/// Simple POST with no body (start/stop/restart)
async fn api_post_action(&self, path: &str) -> Result<()> {
self.api_request("POST", path, None, DEFAULT_TIMEOUT).await?;
self.api_request("POST", path, None, DEFAULT_TIMEOUT)
.await?;
Ok(())
}
@@ -269,11 +275,7 @@ impl PodmanClient {
Ok(())
}
pub async fn create_container(
&self,
manifest: &AppManifest,
name: &str,
) -> Result<String> {
pub async fn create_container(&self, manifest: &AppManifest, name: &str) -> Result<String> {
// Build the container spec for the API
let mut port_mappings = Vec::new();
for port in &manifest.app.ports {
@@ -341,14 +343,12 @@ impl PodmanClient {
},
});
let result = self.api_request(
"POST",
"libpod/containers/create",
Some(body),
LONG_TIMEOUT,
).await?;
let result = self
.api_request("POST", "libpod/containers/create", Some(body), LONG_TIMEOUT)
.await?;
let id = result["Id"].as_str()
let id = result["Id"]
.as_str()
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.context("Podman API returned no container ID — creation may have failed")?;
@@ -357,7 +357,8 @@ impl PodmanClient {
}
pub async fn start_container(&self, name: &str) -> Result<()> {
self.api_post_action(&format!("libpod/containers/{}/start", name)).await
self.api_post_action(&format!("libpod/containers/{}/start", name))
.await
}
pub async fn stop_container(&self, name: &str) -> Result<()> {
@@ -366,7 +367,9 @@ impl PodmanClient {
&format!("libpod/containers/{}/stop?t=10", name),
None,
DEFAULT_TIMEOUT,
).await.map(|_| ())
)
.await
.map(|_| ())
}
pub async fn restart_container(&self, name: &str) -> Result<()> {
@@ -375,7 +378,9 @@ impl PodmanClient {
&format!("libpod/containers/{}/restart?t=10", name),
None,
DEFAULT_TIMEOUT,
).await.map(|_| ())
)
.await
.map(|_| ())
}
pub async fn remove_container(&self, name: &str) -> Result<()> {
@@ -384,19 +389,25 @@ impl PodmanClient {
&format!("libpod/containers/{}?force=true", name),
None,
DEFAULT_TIMEOUT,
).await.map(|_| ())
)
.await
.map(|_| ())
}
pub async fn get_container_status(&self, name: &str) -> Result<ContainerStatus> {
let data = self.api_request(
"GET",
&format!("libpod/containers/{}/json", name),
None,
DEFAULT_TIMEOUT,
).await?;
let data = self
.api_request(
"GET",
&format!("libpod/containers/{}/json", name),
None,
DEFAULT_TIMEOUT,
)
.await?;
let state_str = data["State"]["Status"].as_str().unwrap_or("unknown");
let health = data["State"]["Health"]["Status"].as_str().map(|s| s.to_string());
let health = data["State"]["Health"]["Status"]
.as_str()
.map(|s| s.to_string());
let started_at = data["State"]["StartedAt"].as_str().map(|s| s.to_string());
let container_name = data["Name"].as_str().unwrap_or(name).to_string();
@@ -413,9 +424,11 @@ impl PodmanClient {
health,
exit_code,
started_at,
image: data["ImageName"].as_str()
image: data["ImageName"]
.as_str()
.or_else(|| data["Config"]["Image"].as_str())
.unwrap_or("").to_string(),
.unwrap_or("")
.to_string(),
created: data["Created"].as_str().unwrap_or("").to_string(),
ports,
lan_address,
@@ -450,32 +463,45 @@ impl PodmanClient {
}
pub async fn list_containers(&self) -> Result<Vec<ContainerStatus>> {
let data = self.api_request(
"GET",
"libpod/containers/json?all=true",
None,
DEFAULT_TIMEOUT,
).await?;
let data = self
.api_request(
"GET",
"libpod/containers/json?all=true",
None,
DEFAULT_TIMEOUT,
)
.await?;
let containers = data.as_array()
let containers = data
.as_array()
.ok_or_else(|| anyhow::anyhow!("Expected array from containers/json"))?;
let mut result = Vec::with_capacity(containers.len());
for c in containers {
let name = if let Some(names) = c["Names"].as_array() {
names.get(0).and_then(|v| v.as_str()).unwrap_or("").to_string()
names
.first()
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string()
} else {
c["Names"].as_str().unwrap_or("").to_string()
};
let ports = if let Some(ports_array) = c["Ports"].as_array() {
ports_array.iter().filter_map(|port| {
let host_port = port["host_port"].as_u64()?;
let container_port = port["container_port"].as_u64()?;
let protocol = port["protocol"].as_str().unwrap_or("tcp");
Some(format!("0.0.0.0:{}->{}/{}", host_port, container_port, protocol))
}).collect()
ports_array
.iter()
.filter_map(|port| {
let host_port = port["host_port"].as_u64()?;
let container_port = port["container_port"].as_u64()?;
let protocol = port["protocol"].as_str().unwrap_or("tcp");
Some(format!(
"0.0.0.0:{}->{}/{}",
host_port, container_port, protocol
))
})
.collect()
} else {
vec![]
};
@@ -483,12 +509,14 @@ impl PodmanClient {
let status_str = c["Status"].as_str().unwrap_or("");
let health = parse_health_from_status(status_str)
.or_else(|| c["Health"].as_str().map(|s| s.to_string()));
let started_at = c["StartedAt"].as_str()
let started_at = c["StartedAt"]
.as_str()
.or_else(|| c["Started"].as_str())
.map(|s| s.to_string());
let lan_address = Self::lan_address_for(&name);
let exit_code = c["ExitCode"].as_i64()
let exit_code = c["ExitCode"]
.as_i64()
.or_else(|| c["State"]["ExitCode"].as_i64())
.map(|c| c as i32);
@@ -511,9 +539,14 @@ impl PodmanClient {
/// Check if the Podman socket is available and responding.
pub async fn health_check(&self) -> bool {
self.api_request("GET", "libpod/info", None, std::time::Duration::from_secs(5))
.await
.is_ok()
self.api_request(
"GET",
"libpod/info",
None,
std::time::Duration::from_secs(5),
)
.await
.is_ok()
}
}
@@ -540,11 +573,23 @@ fn parse_port_bindings(bindings: &serde_json::Value) -> Vec<String> {
fn parse_memory_limit(limit: &str) -> Option<i64> {
let limit = limit.trim().to_lowercase();
if limit.ends_with('g') {
limit.trim_end_matches('g').parse::<f64>().ok().map(|v| (v * 1_073_741_824.0) as i64)
limit
.trim_end_matches('g')
.parse::<f64>()
.ok()
.map(|v| (v * 1_073_741_824.0) as i64)
} else if limit.ends_with('m') {
limit.trim_end_matches('m').parse::<f64>().ok().map(|v| (v * 1_048_576.0) as i64)
limit
.trim_end_matches('m')
.parse::<f64>()
.ok()
.map(|v| (v * 1_048_576.0) as i64)
} else if limit.ends_with('k') {
limit.trim_end_matches('k').parse::<f64>().ok().map(|v| (v * 1024.0) as i64)
limit
.trim_end_matches('k')
.parse::<f64>()
.ok()
.map(|v| (v * 1024.0) as i64)
} else {
limit.parse::<i64>().ok()
}

View File

@@ -29,8 +29,14 @@ impl PortManager {
/// 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().map_err(|e| PortError::LockPoisoned(e.to_string()))?;
let mut port_to_app = self.port_to_app.write().map_err(|e| PortError::LockPoisoned(e.to_string()))?;
let mut allocations = self
.allocations
.write()
.map_err(|e| PortError::LockPoisoned(e.to_string()))?;
let mut port_to_app = self
.port_to_app
.write()
.map_err(|e| PortError::LockPoisoned(e.to_string()))?;
let mut allocated_ports = Vec::new();
// Check for conflicts and allocate ports
@@ -56,22 +62,33 @@ impl PortManager {
/// Get allocated ports for an app
pub fn get_port_mapping(&self, app_id: &str) -> Result<Option<Vec<u16>>, PortError> {
let allocations = self.allocations.read().map_err(|e| PortError::LockPoisoned(e.to_string()))?;
let allocations = self
.allocations
.read()
.map_err(|e| PortError::LockPoisoned(e.to_string()))?;
Ok(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) -> Result<Option<u16>, PortError> {
Ok(self.get_port_mapping(app_id)?
.and_then(|ports| {
ports.iter().find(|&&p| p == base_port + self.port_offset).copied()
}))
Ok(self.get_port_mapping(app_id)?.and_then(|ports| {
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().map_err(|e| PortError::LockPoisoned(e.to_string()))?;
let mut port_to_app = self.port_to_app.write().map_err(|e| PortError::LockPoisoned(e.to_string()))?;
let mut allocations = self
.allocations
.write()
.map_err(|e| PortError::LockPoisoned(e.to_string()))?;
let mut port_to_app = self
.port_to_app
.write()
.map_err(|e| PortError::LockPoisoned(e.to_string()))?;
if let Some(ports) = allocations.remove(app_id) {
for port in ports {
@@ -86,13 +103,19 @@ impl PortManager {
/// Check if a port is available
pub fn is_port_available(&self, base_port: u16) -> Result<bool, PortError> {
let dev_port = base_port + self.port_offset;
let port_to_app = self.port_to_app.read().map_err(|e| PortError::LockPoisoned(e.to_string()))?;
let port_to_app = self
.port_to_app
.read()
.map_err(|e| PortError::LockPoisoned(e.to_string()))?;
Ok(!port_to_app.contains_key(&dev_port))
}
/// Get all allocated ports
pub fn get_all_allocations(&self) -> Result<HashMap<String, Vec<u16>>, PortError> {
let allocations = self.allocations.read().map_err(|e| PortError::LockPoisoned(e.to_string()))?;
let allocations = self
.allocations
.read()
.map_err(|e| PortError::LockPoisoned(e.to_string()))?;
Ok(allocations.clone())
}

View File

@@ -1,5 +1,5 @@
use crate::manifest::AppManifest;
use crate::podman_client::{ContainerStatus, ContainerState, PodmanClient};
use crate::podman_client::{ContainerState, ContainerStatus, PodmanClient};
use anyhow::{Context, Result};
use async_trait::async_trait;
use std::process::Command;
@@ -49,7 +49,7 @@ impl ContainerRuntime for PodmanRuntime {
// 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;
port.host += port_offset;
}
// PodmanClient doesn't take port_offset, so we use the modified manifest
@@ -98,7 +98,6 @@ impl DockerRuntime {
}
cmd
}
}
#[async_trait]
@@ -143,14 +142,16 @@ impl ContainerRuntime for DockerRuntime {
// Docker uses bridge network by default
}
_ => {
cmd.arg("--network").arg(&manifest.app.security.network_policy);
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));
cmd.arg("-p")
.arg(format!("{}:{}", host_port, port.container));
}
// Volumes
@@ -189,19 +190,14 @@ impl ContainerRuntime for DockerRuntime {
cmd.arg(&manifest.app.container.image);
let output = cmd
.output()
.await
.context("Failed to create container")?;
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();
let container_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(container_id)
}
@@ -210,10 +206,7 @@ impl ContainerRuntime for DockerRuntime {
let mut cmd = self.docker_async();
cmd.arg("start").arg(name);
let output = cmd
.output()
.await
.context("Failed to start container")?;
let output = cmd.output().await.context("Failed to start container")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
@@ -227,10 +220,7 @@ impl ContainerRuntime for DockerRuntime {
let mut cmd = self.docker_async();
cmd.arg("stop").arg(name);
let output = cmd
.output()
.await
.context("Failed to stop container")?;
let output = cmd.output().await.context("Failed to stop container")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
@@ -244,10 +234,7 @@ impl ContainerRuntime for DockerRuntime {
let mut cmd = self.docker_async();
cmd.arg("rm").arg("-f").arg(name);
let output = cmd
.output()
.await
.context("Failed to remove container")?;
let output = cmd.output().await.context("Failed to remove container")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
@@ -264,10 +251,7 @@ impl ContainerRuntime for DockerRuntime {
.arg("{{.Id}}|{{.Name}}|{{.State.Status}}|{{.Config.Image}}|{{.Created}}|{{.NetworkSettings.Ports}}")
.arg(name);
let output = cmd
.output()
.await
.context("Failed to inspect container")?;
let output = cmd.output().await.context("Failed to inspect container")?;
if !output.status.success() {
return Err(anyhow::anyhow!("Container not found: {}", name));
@@ -301,10 +285,7 @@ impl ContainerRuntime for DockerRuntime {
.arg(lines.to_string())
.arg(name);
let output = cmd
.output()
.await
.context("Failed to get container logs")?;
let output = cmd.output().await.context("Failed to get container logs")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
@@ -317,15 +298,9 @@ impl ContainerRuntime for DockerRuntime {
async fn list_containers(&self) -> Result<Vec<ContainerStatus>> {
let mut cmd = self.docker_async();
cmd.arg("ps")
.arg("-a")
.arg("--format")
.arg("json");
cmd.arg("ps").arg("-a").arg("--format").arg("json");
let output = cmd
.output()
.await
.context("Failed to list containers")?;
let output = cmd.output().await.context("Failed to list containers")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
@@ -334,16 +309,16 @@ impl ContainerRuntime for DockerRuntime {
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("");
@@ -352,13 +327,11 @@ impl ContainerRuntime for DockerRuntime {
} 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")
),
state: ContainerState::from(container["State"].as_str().unwrap_or("unknown")),
health: None,
exit_code: container["ExitCode"].as_i64().map(|c| c as i32),
started_at: None,
@@ -389,24 +362,16 @@ impl AutoRuntime {
runtime: Box::new(DockerRuntime::new(user)),
})
} else {
Err(anyhow::anyhow!(
"Neither Podman nor Docker is available"
))
Err(anyhow::anyhow!("Neither Podman nor Docker is available"))
}
}
fn check_podman_available() -> bool {
Command::new("podman")
.arg("--version")
.output()
.is_ok()
Command::new("podman").arg("--version").output().is_ok()
}
fn check_docker_available() -> bool {
Command::new("docker")
.arg("--version")
.output()
.is_ok()
Command::new("docker").arg("--version").output().is_ok()
}
}
@@ -422,7 +387,9 @@ impl ContainerRuntime for AutoRuntime {
name: &str,
port_offset: u16,
) -> Result<String> {
self.runtime.create_container(manifest, name, port_offset).await
self.runtime
.create_container(manifest, name, port_offset)
.await
}
async fn start_container(&self, name: &str) -> Result<()> {