podi/
lib.rs

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
38/// Represents a podman container.
39///
40/// This struct abstracts starting, stopping, and removing the container.
41/// The generic callback `F` allows running custom logic after the container is first created.
42pub 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    /// Creates a new container instance.
59    ///
60    /// # Arguments
61    ///
62    /// - `name` - The name of the container.
63    /// - `image` - The container image to use.
64    /// - `args` - Additional arguments to pass to the `podman run` command.
65    /// - `callback` - A callback function executed after the container starts.
66    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    /// Starts the container.
81    ///
82    /// - If the container is already running, this is a no-op.
83    /// - If the container is stopped, paused, or exited, it will be started.
84    /// - If the container does not exist, it will be created and started,
85    ///   and the callback function will be run.
86    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    /// Stops the container.
111    /// Also removes it if `remove` is true.
112    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}