use std::process::Command;
use std::sync::Mutex;
use crate::error::PodboxError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PodmanVersion {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl PodmanVersion {
pub fn at_least(&self, major: u32, minor: u32) -> bool {
(self.major, self.minor) >= (major, minor)
}
}
static PODMAN_VERSION: Mutex<Option<PodmanVersion>> = Mutex::new(None);
fn parse_version_string(s: &str) -> PodmanVersion {
let s = s.trim();
let s = s.split_whitespace().next().unwrap_or(s);
let s = s.rsplit_once(':').map(|(_, after)| after).unwrap_or(s);
let mut parts = s.splitn(3, '.');
PodmanVersion {
major: parts.next().and_then(|p| p.parse().ok()).unwrap_or(0),
minor: parts.next().and_then(|p| p.parse().ok()).unwrap_or(0),
patch: parts.next().and_then(|p| p.parse().ok()).unwrap_or(0),
}
}
pub fn podman_version() -> anyhow::Result<PodmanVersion> {
let mut cache = PODMAN_VERSION.lock().unwrap();
if let Some(ref ver) = *cache {
return Ok(ver.clone());
}
let structured = Command::new("podman")
.args(["version", "-f", "{{.Client.Version}}"])
.output()
.ok()
.filter(|o| o.status.success());
let version_str = match structured {
Some(ref output) => String::from_utf8_lossy(&output.stdout).trim().to_string(),
None => {
let output = Command::new("podman").args(["--version"]).output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
stdout.split_whitespace().last().unwrap_or("").to_string()
}
};
let ver = parse_version_string(&version_str);
*cache = Some(ver.clone());
Ok(ver)
}
#[cfg(test)]
#[allow(dead_code)]
pub(crate) fn set_test_version(ver: PodmanVersion) {
let mut cache = PODMAN_VERSION.lock().unwrap();
*cache = Some(ver);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_standard_version() {
let v = parse_version_string("5.3.0");
assert_eq!(
v,
PodmanVersion {
major: 5,
minor: 3,
patch: 0
}
);
}
#[test]
fn parses_version_with_epoch() {
let v = parse_version_string("100:5.3.0");
assert_eq!(
v,
PodmanVersion {
major: 5,
minor: 3,
patch: 0
}
);
}
#[test]
fn parses_version_with_parenthetical() {
let v = parse_version_string("5.3.0 (ok)");
assert_eq!(
v,
PodmanVersion {
major: 5,
minor: 3,
patch: 0
}
);
}
#[test]
fn parses_version_with_leading_trailing_whitespace() {
let v = parse_version_string(" 5.3.0 ");
assert_eq!(
v,
PodmanVersion {
major: 5,
minor: 3,
patch: 0
}
);
}
#[test]
fn handles_single_component() {
let v = parse_version_string("5");
assert_eq!(
v,
PodmanVersion {
major: 5,
minor: 0,
patch: 0
}
);
}
#[test]
fn handles_two_components() {
let v = parse_version_string("5.3");
assert_eq!(
v,
PodmanVersion {
major: 5,
minor: 3,
patch: 0
}
);
}
#[test]
fn handles_empty_string() {
let v = parse_version_string("");
assert_eq!(
v,
PodmanVersion {
major: 0,
minor: 0,
patch: 0
}
);
}
#[test]
fn handles_non_numeric() {
let v = parse_version_string("abc");
assert_eq!(
v,
PodmanVersion {
major: 0,
minor: 0,
patch: 0
}
);
}
#[test]
fn handles_partial_numeric() {
let v = parse_version_string("5.abc.0");
assert_eq!(
v,
PodmanVersion {
major: 5,
minor: 0,
patch: 0
}
);
}
#[test]
fn version_at_least_works() {
let v = PodmanVersion {
major: 5,
minor: 6,
patch: 0,
};
assert!(v.at_least(5, 6));
assert!(v.at_least(4, 0));
assert!(v.at_least(5, 5));
assert!(!v.at_least(5, 7));
assert!(!v.at_least(6, 0));
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum ContainerState {
Running,
Stopped,
Missing,
}
pub fn image_exists(tag: &str) -> anyhow::Result<bool> {
let output = std::process::Command::new("podman")
.args(["image", "exists", tag])
.output()?;
Ok(output.status.success())
}
pub fn image_labels(tag: &str) -> anyhow::Result<std::collections::HashMap<String, String>> {
let output = std::process::Command::new("podman")
.args([
"inspect",
"--type",
"image",
"--format",
"{{json .Labels}}",
tag,
])
.output()?;
if !output.status.success() {
return Ok(std::collections::HashMap::new());
}
let stdout = String::from_utf8_lossy(&output.stdout);
let map: std::collections::HashMap<String, String> =
serde_json::from_str(stdout.trim()).unwrap_or_default();
Ok(map)
}
pub fn query_state(name: &str) -> anyhow::Result<ContainerState> {
let output = Command::new("podman")
.args([
"inspect",
"--type",
"container",
"--format",
"{{.State.Status}}",
name,
])
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("no such container") || stderr.contains("no such object") {
if let Ok(sys) = Command::new("systemctl")
.args(["--user", "is-active", &format!("{}.service", name)])
.output()
{
let state = String::from_utf8_lossy(&sys.stdout).trim().to_string();
match state.as_str() {
"active" => {
if let Ok(out) = Command::new("podman")
.args(["ps", "-q", "--filter", &format!("name=^{name}$")])
.output()
{
if out.status.success()
&& !String::from_utf8_lossy(&out.stdout).trim().is_empty()
{
return Ok(ContainerState::Running);
}
}
return Ok(ContainerState::Stopped);
}
"inactive" | "failed" => return Ok(ContainerState::Stopped),
_ => {
if crate::quadlet_install::is_installed(name) {
return Ok(ContainerState::Stopped);
}
return Ok(ContainerState::Missing);
}
};
}
if crate::quadlet_install::is_installed(name) {
return Ok(ContainerState::Stopped);
}
return Ok(ContainerState::Missing);
}
return Err(PodboxError::PodmanInspectFailed {
name: name.into(),
stderr: stderr.to_string(),
}
.into());
}
let stdout = String::from_utf8_lossy(&output.stdout)
.trim()
.to_lowercase();
match stdout.as_str() {
"running" => Ok(ContainerState::Running),
"stopped" | "exited" => Ok(ContainerState::Stopped),
_ => Ok(ContainerState::Stopped),
}
}
pub fn image_digest(tag: &str) -> anyhow::Result<String> {
let output = Command::new("podman")
.args(["inspect", "--format", "{{.Digest}}", tag])
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(PodboxError::PodmanInspectFailed {
name: tag.into(),
stderr: stderr.to_string(),
}
.into());
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}