1use std::{
2 fmt, io,
3 process::{Command, Output},
4};
5
6static CMD: &str = "podman";
7
8#[derive(Debug)]
9pub enum ContainerError<E> {
10 Io(io::Error),
11 CommandFailed(String),
12 UnexpectedState(String),
13 CallbackFailed(E),
14}
15
16impl<E> From<io::Error> for ContainerError<E> {
17 fn from(e: io::Error) -> Self {
18 Self::Io(e)
19 }
20}
21
22impl<E> fmt::Display for ContainerError<E>
23where
24 E: std::error::Error,
25{
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 match self {
28 ContainerError::Io(e) => write!(f, "IO error: {e}"),
29 ContainerError::CommandFailed(msg) => write!(f, "Command failed: {msg}"),
30 ContainerError::UnexpectedState(state) => {
31 write!(f, "Unexpected container state: {state}")
32 }
33 ContainerError::CallbackFailed(e) => write!(f, "Callback failed: {e}"),
34 }
35 }
36}
37
38pub struct Container<F, E>
43where
44 F: Fn() -> Result<(), E>,
45 E: std::error::Error,
46{
47 name: String,
48 image: String,
49 args: Vec<String>,
50 callback: F,
51}
52
53impl<F, E> Container<F, E>
54where
55 F: Fn() -> Result<(), E>,
56 E: std::error::Error,
57{
58 pub fn new<N, I, A>(name: N, image: I, args: Vec<A>, callback: F) -> Self
67 where
68 N: Into<String>,
69 I: Into<String>,
70 A: Into<String>,
71 {
72 Self {
73 name: name.into(),
74 image: image.into(),
75 args: args.into_iter().map(Into::into).collect(),
76 callback,
77 }
78 }
79
80 pub fn start(&self) -> Result<(), ContainerError<E>> {
87 let output = output_command(&[
88 "container",
89 "inspect",
90 &self.name,
91 "--format",
92 "{{.State.Status}}",
93 ])?;
94 if output.status.success() {
95 match String::from_utf8_lossy(&output.stdout).trim() {
96 "running" => Ok(()),
97 "created" | "exited" | "paused" => spawn_command(&["start", &self.name]),
98 state => Err(ContainerError::UnexpectedState(state.to_string())),
99 }
100 } else {
101 let full_args: Vec<&str> = std::iter::once("run")
102 .chain(self.args.iter().map(String::as_str))
103 .chain(["--name", &self.name, "--detach", &self.image])
104 .collect();
105 spawn_command(&full_args)?;
106 (self.callback)().map_err(ContainerError::CallbackFailed)
107 }
108 }
109
110 pub fn stop(&self, remove: bool) -> Result<(), ContainerError<E>> {
113 if remove {
114 spawn_command(&["rm", "--force", &self.name])
115 } else {
116 spawn_command(&["stop", &self.name])
117 }
118 }
119}
120
121fn spawn_command<E>(args: &[&str]) -> Result<(), ContainerError<E>> {
122 if Command::new(CMD).args(args).status()?.success() {
123 Ok(())
124 } else {
125 Err(ContainerError::CommandFailed(format!(
126 "Command failed: {} {}",
127 CMD,
128 args.join(" ")
129 )))
130 }
131}
132
133fn output_command(args: &[&str]) -> Result<Output, io::Error> {
134 Command::new(CMD).args(args).output()
135}