chore(release): stage v1.7.55-alpha
This commit is contained in:
@@ -46,6 +46,7 @@ pub struct ContainerStatus {
|
||||
pub enum ContainerState {
|
||||
Created,
|
||||
Running,
|
||||
Stopping,
|
||||
Stopped,
|
||||
Exited,
|
||||
Paused,
|
||||
@@ -57,6 +58,7 @@ impl From<&str> for ContainerState {
|
||||
match s.to_lowercase().as_str() {
|
||||
"created" => ContainerState::Created,
|
||||
"running" => ContainerState::Running,
|
||||
"stopping" => ContainerState::Stopping,
|
||||
"stopped" => ContainerState::Stopped,
|
||||
"exited" => ContainerState::Exited,
|
||||
"paused" => ContainerState::Paused,
|
||||
@@ -120,6 +122,7 @@ impl PodmanClient {
|
||||
"penpot" => "http://localhost:9001",
|
||||
"nextcloud" => "http://localhost:8085",
|
||||
"vaultwarden" => "http://localhost:8082",
|
||||
"gitea" => "http://localhost:3001",
|
||||
"jellyfin" => "http://localhost:8096",
|
||||
"photoprism" => "http://localhost:2342",
|
||||
"immich_server" | "immich" => "http://localhost:2283",
|
||||
@@ -130,7 +133,7 @@ impl PodmanClient {
|
||||
"fedimint" | "fedimintd" => "http://localhost:8175",
|
||||
"fedimint-gateway" => "http://localhost:8176",
|
||||
"nostr-rs-relay" => "http://localhost:18081",
|
||||
"indeedhub" => "http://localhost:7777",
|
||||
"indeedhub" => "http://localhost:7778",
|
||||
"dwn" => "http://localhost:3100",
|
||||
"endurain" => "http://localhost:8080",
|
||||
"electrs" | "archy-electrs-ui" => "http://localhost:50002",
|
||||
|
||||
@@ -3,8 +3,12 @@ use crate::podman_client::{ContainerState, ContainerStatus, PodmanClient};
|
||||
use anyhow::{Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use tokio::process::Command as TokioCommand;
|
||||
|
||||
const PODMAN_CLI_DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
const PODMAN_CLI_BUILD_TIMEOUT: Duration = Duration::from_secs(900);
|
||||
|
||||
#[async_trait]
|
||||
pub trait ContainerRuntime: Send + Sync {
|
||||
async fn pull_image(&self, image: &str, signature: Option<&str>) -> Result<()>;
|
||||
@@ -52,13 +56,31 @@ impl PodmanRuntime {
|
||||
/// Run `podman <args>`, returning an error with captured stderr on non-zero
|
||||
/// exit. Used for operations (build, image inspect) that are awkward over the
|
||||
/// HTTP API. The daemon runs as the target user already, so no sudo hop.
|
||||
async fn podman_cli(&self, args: &[&str]) -> Result<std::process::Output> {
|
||||
async fn podman_cli_timeout(
|
||||
&self,
|
||||
args: &[&str],
|
||||
timeout: Duration,
|
||||
) -> Result<std::process::Output> {
|
||||
let mut cmd = TokioCommand::new("podman");
|
||||
cmd.args(args);
|
||||
cmd.output()
|
||||
cmd.kill_on_drop(true);
|
||||
tokio::time::timeout(timeout, cmd.output())
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"podman {} timed out after {}s",
|
||||
args.join(" "),
|
||||
timeout.as_secs()
|
||||
)
|
||||
})?
|
||||
.with_context(|| format!("failed to execute podman {}", args.join(" ")))
|
||||
}
|
||||
|
||||
/// Run `podman <args>` with a short timeout for control-plane operations.
|
||||
async fn podman_cli(&self, args: &[&str]) -> Result<std::process::Output> {
|
||||
self.podman_cli_timeout(args, PODMAN_CLI_DEFAULT_TIMEOUT)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -84,19 +106,72 @@ impl ContainerRuntime for PodmanRuntime {
|
||||
}
|
||||
|
||||
async fn start_container(&self, name: &str) -> Result<()> {
|
||||
self.client.start_container(name).await
|
||||
match self.client.start_container(name).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(api_err) => {
|
||||
let output = self.podman_cli(&["start", name]).await?;
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
Err(api_err.context(format!("podman start fallback failed: {}", stderr.trim())))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn stop_container(&self, name: &str) -> Result<()> {
|
||||
self.client.stop_container(name).await
|
||||
match self.client.stop_container(name).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(api_err) => {
|
||||
let output = self.podman_cli(&["stop", "-t", "30", name]).await?;
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if is_missing_container_error(&stderr) {
|
||||
return Ok(());
|
||||
}
|
||||
Err(api_err.context(format!("podman stop fallback failed: {}", stderr.trim())))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_container(&self, name: &str) -> Result<()> {
|
||||
self.client.remove_container(name).await
|
||||
match self.client.remove_container(name).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(api_err) => {
|
||||
let output = self.podman_cli(&["rm", "-f", name]).await?;
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if is_missing_container_error(&stderr) {
|
||||
return Ok(());
|
||||
}
|
||||
Err(api_err.context(format!("podman rm fallback failed: {}", stderr.trim())))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_container_status(&self, name: &str) -> Result<ContainerStatus> {
|
||||
self.client.get_container_status(name).await
|
||||
match self.client.get_container_status(name).await {
|
||||
Ok(status) => Ok(status),
|
||||
Err(api_err) => {
|
||||
let output = self
|
||||
.podman_cli(&["container", "inspect", "--format", "json", name])
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(api_err
|
||||
.context(format!("podman inspect fallback failed: {}", stderr.trim())));
|
||||
}
|
||||
parse_podman_inspect_json(&output.stdout, name)
|
||||
.with_context(|| format!("podman API inspect failed: {api_err}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_container_logs(&self, name: &str, lines: u32) -> Result<Vec<String>> {
|
||||
@@ -142,7 +217,9 @@ impl ContainerRuntime for PodmanRuntime {
|
||||
async fn build_image(&self, config: &BuildConfig) -> Result<()> {
|
||||
let args = build_args_for_podman(config);
|
||||
let borrowed: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
|
||||
let output = self.podman_cli(&borrowed).await?;
|
||||
let output = self
|
||||
.podman_cli_timeout(&borrowed, PODMAN_CLI_BUILD_TIMEOUT)
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
@@ -211,6 +288,103 @@ fn parse_podman_ps_json(stdout: &[u8]) -> Result<Vec<ContainerStatus>> {
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn parse_podman_inspect_json(stdout: &[u8], requested_name: &str) -> Result<ContainerStatus> {
|
||||
let text = String::from_utf8_lossy(stdout);
|
||||
let containers: Vec<serde_json::Value> = serde_json::from_str(&text)?;
|
||||
let c = containers
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("podman inspect returned no containers"))?;
|
||||
if c.get("State").is_none() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"podman inspect returned non-container object for {requested_name}"
|
||||
));
|
||||
}
|
||||
let name = c
|
||||
.get("Name")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.trim_start_matches('/'))
|
||||
.unwrap_or(requested_name)
|
||||
.to_string();
|
||||
let state = c
|
||||
.get("State")
|
||||
.and_then(|v| v.get("Status"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown");
|
||||
Ok(ContainerStatus {
|
||||
id: c
|
||||
.get("Id")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
name: name.clone(),
|
||||
state: ContainerState::from(state),
|
||||
health: c
|
||||
.get("State")
|
||||
.and_then(|v| v.get("Health"))
|
||||
.and_then(|v| v.get("Status"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string()),
|
||||
exit_code: c
|
||||
.get("State")
|
||||
.and_then(|v| v.get("ExitCode"))
|
||||
.and_then(|v| v.as_i64())
|
||||
.map(|c| c as i32),
|
||||
started_at: c
|
||||
.get("State")
|
||||
.and_then(|v| v.get("StartedAt"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string()),
|
||||
image: c
|
||||
.get("ImageName")
|
||||
.and_then(|v| v.as_str())
|
||||
.or_else(|| {
|
||||
c.get("Config")
|
||||
.and_then(|v| v.get("Image"))
|
||||
.and_then(|v| v.as_str())
|
||||
})
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
created: c
|
||||
.get("Created")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
ports: parse_inspect_ports(c),
|
||||
lan_address: PodmanClient::lan_address_for(&name),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_inspect_ports(c: &serde_json::Value) -> Vec<String> {
|
||||
let Some(bindings) = c
|
||||
.get("HostConfig")
|
||||
.and_then(|v| v.get("PortBindings"))
|
||||
.and_then(|v| v.as_object())
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut ports = Vec::new();
|
||||
for (container_port, host_bindings) in bindings {
|
||||
let Some(host_bindings) = host_bindings.as_array() else {
|
||||
continue;
|
||||
};
|
||||
for binding in host_bindings {
|
||||
let host_ip = binding
|
||||
.get("HostIp")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("0.0.0.0");
|
||||
let host_port = binding
|
||||
.get("HostPort")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
if !host_port.is_empty() {
|
||||
ports.push(format!("{host_ip}:{host_port}->{container_port}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
ports
|
||||
}
|
||||
|
||||
fn parse_podman_ps_ports(ports: Option<&serde_json::Value>) -> Vec<String> {
|
||||
ports
|
||||
.and_then(|v| v.as_array())
|
||||
@@ -237,6 +411,14 @@ fn parse_health_from_status(status: &str) -> Option<String> {
|
||||
(start < end).then(|| status[start + 1..end].to_string())
|
||||
}
|
||||
|
||||
fn is_missing_container_error(stderr: &str) -> bool {
|
||||
let stderr = stderr.to_ascii_lowercase();
|
||||
stderr.contains("no container with name or id")
|
||||
|| stderr.contains("no such container")
|
||||
|| stderr.contains("does not exist")
|
||||
|| stderr.contains("not found")
|
||||
}
|
||||
|
||||
/// Build the argv for `podman build` from a BuildConfig.
|
||||
///
|
||||
/// Extracted so it can be unit-tested without actually invoking podman.
|
||||
|
||||
Reference in New Issue
Block a user