///|
/// Running `podman`. Every command funnels through here so `--dry-run`,
/// `--podman-args` and per-subcommand argument injection are honoured in one
/// place.
///|
/// A configured podman invocation.
pub struct Podman {
path : String
dry_run : Bool
verbose : Bool
global_args : Array[String]
cmd_args : Map[String, Array[String]]
}
///|
pub fn new_podman(g : GlobalOptions) -> Podman {
{
path: g.podman_path,
dry_run: g.dry_run,
verbose: g.verbose,
global_args: g.podman_args,
cmd_args: g.podman_cmd_args,
}
}
///|
/// The arguments after the podman binary: global options, the subcommand, then
/// the command's configured options and the caller's own.
pub fn podman_argv(
podman : Podman,
cmd : String,
args : Array[String],
) -> Array[String] {
let out : Array[String] = []
out.append(podman.global_args)
if cmd.length() > 0 {
out.push(cmd)
}
let lookup = if cmd == "create" { "run" } else { cmd }
match podman.cmd_args.get(lookup) {
Some(extra) => out.append(extra)
None => ()
}
out.append(args)
out
}
///|
/// Print the command that would run, for `--dry-run` and `--verbose`.
fn log_command(podman : Podman, argv : Array[String]) -> Unit {
if podman.dry_run || podman.verbose {
let line = podman.path + " " + argv.join(" ")
println(line)
}
}
///|
/// Run podman, inheriting the terminal. Returns the exit code.
pub async fn podman_run(
podman : Podman,
cmd : String,
args : Array[String],
) -> Int32 {
let argv = podman_argv(podman, cmd, args)
log_command(podman, argv)
if podman.dry_run {
return 0
}
@process.run(
podman.path,
argv,
stdin=@stdio.stdin,
stdout=@stdio.stdout,
stderr=@stdio.stderr,
) catch {
e => {
eprintln("podcomp: failed to run \{podman.path}: \{error_text(e)}")
1
}
}
}
///|
/// Run podman and capture its standard output. Raises when the exit code is
/// non-zero, with the captured standard error as the message.
pub async fn podman_output(
podman : Podman,
cmd : String,
args : Array[String],
) -> String raise Error {
let argv = podman_argv(podman, cmd, args)
log_command(podman, argv)
if podman.dry_run {
return ""
}
let (code, stdout, stderr) = @process.collect_output(podman.path, argv) catch {
e => raise fail("failed to run \{podman.path}: \{error_text(e)}")
}
if code != 0 {
let message = (stderr.text() catch { _ => "" }).trim().to_owned()
if message.length() > 0 {
raise fail(message)
}
raise fail("\{podman.path} \{cmd} exited with status \{code}")
}
stdout.text() catch {
_ => ""
}
}
///|
/// Run podman and capture its standard output, returning `None` when it fails.
/// Used for existence probes (`podman inspect`, `podman network exists`).
pub async fn podman_try_output(
podman : Podman,
cmd : String,
args : Array[String],
) -> String? {
let argv = podman_argv(podman, cmd, args)
log_command(podman, argv)
if podman.dry_run {
return None
}
let (code, stdout, _) = @process.collect_output(podman.path, argv) catch {
_ => return None
}
if code != 0 {
None
} else {
Some(stdout.text() catch { _ => "" })
}
}
///|
/// The podman version string, or `None` when podman is unavailable.
pub async fn podman_version(podman : Podman) -> String? {
if podman.dry_run {
return None
}
let (code, stdout, _) = @process.collect_output(podman.path, ["--version"]) catch {
_ => return None
}
if code != 0 {
return None
}
let text = (stdout.text() catch { _ => "" }).trim().to_owned()
let parts = text.split(" ").to_array()
if parts.length() == 0 {
None
} else {
Some(parts[parts.length() - 1].to_owned())
}
}
///|
/// Whether podman is on the path and runnable.
pub async fn podman_available(podman : Podman) -> Bool {
match podman_version(podman) {
Some(_) => true
None => false
}
}
///|
/// Whether a dotted version string is at least `want` (`"5.6.0"`).
pub fn version_at_least(have : String, want : String) -> Bool {
let h = have.split(".").to_array()
let w = want.split(".").to_array()
let n = if h.length() < w.length() { h.length() } else { w.length() }
for i in 0.. b {
return true
}
if a < b {
return false
}
}
h.length() >= w.length()
}