cnf_lib/environment/
distrobox.rs

1// Copyright (C) 2023 Andreas Hartmann <hartan@7x.de>
2// GNU General Public License v3.0+ (https://www.gnu.org/licenses/gpl-3.0.txt)
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5//! # Distrobox Environment Handler.
6//!
7//! [Distrobox][distrobox] is a collection of shell-scripts that serves the same purpose as
8//! [toolbx][toolbx]: To provide pet-containers for e.g. development purposes
9//!
10//! [distrobox]: https://github.com/89luca89/distrobox
11//! [toolbx]: https://containertoolbx.org/
12use super::prelude::*;
13
14use std::{io::IsTerminal, path::Path};
15
16const DISTROBOX_ENV: &str = "/run/.containersetupdone";
17const CONTAINER_ENV: &str = "/run/.containerenv";
18
19#[derive(PartialEq, Eq, Debug, PartialOrd, Ord, Serialize, Deserialize)]
20pub struct Distrobox {
21    name: String,
22}
23
24impl fmt::Display for Distrobox {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        write!(f, "distrobox '{}'", self.name)
27    }
28}
29
30impl Distrobox {
31    /// Start a Distrobox container with a given name.
32    ///
33    /// Checks if the distrobox container exists and starts it, if necessary. If `None` is given as
34    /// name, will fall back to the default distrobox name and start that instead. Returns an error
35    /// if unsuccessful.
36    pub fn new(name: Option<String>) -> Result<Self, NewDistroboxError> {
37        let name = match name {
38            Some(name) if !name.is_empty() => name,
39            _ => "my-distrobox".to_string(),
40        };
41
42        // Do an optimistic start:
43        // - If the container exists and isn't started, it will be started
44        // - If the container exists and is started, nothing happens
45        // - If the container doesn't exist, we get an error and report that
46        let ret = Self { name: name.clone() };
47        ret.start()
48            .map_err(|e| NewDistroboxError::CannotStart { source: e, name })?;
49        Ok(ret)
50    }
51
52    /// Starts a given distrobox container.
53    ///
54    /// This function is automatically called by `new()` above and should only ever be called when
55    /// creating a `Distrobox` object without using the constructor. This is currently the case
56    /// when executing aliases in `cnf`, as the `Distrobox` instance is deserialized from the
57    /// config in that case.
58    pub fn start(&self) -> Result<(), StartDistroboxError> {
59        let output = std::process::Command::new("podman")
60            .args(["start", &self.name])
61            .output()
62            .map_err(|e| match e.kind() {
63                std::io::ErrorKind::NotFound => StartDistroboxError::NeedPodman,
64                _ => StartDistroboxError::IoError(e),
65            })?;
66        if output.status.success() {
67            // All good
68            Ok(())
69        } else {
70            let matcher = OutputMatcher::new(&output);
71            if matcher.starts_with("Error: no container with name or ID")
72                && matcher.contains("found: no such container")
73            {
74                Err(StartDistroboxError::NonExistent(self.name.clone()))
75            } else {
76                Err(StartDistroboxError::Podman(output))
77            }
78        }
79    }
80
81    /// Get the Toolbx container currently executing CNF.
82    ///
83    /// Will return an error if the current execution environment isn't Toolbx.
84    pub fn current() -> Result<Self, CurrentDistroboxError> {
85        if !detect() {
86            return Err(CurrentDistroboxError::NotAToolbx);
87        }
88
89        let content = std::fs::read_to_string(CONTAINER_ENV).map_err(|e| {
90            CurrentDistroboxError::Environment {
91                env_file: CONTAINER_ENV.to_string(),
92                source: e,
93            }
94        })?;
95        let name = content
96            .lines()
97            .find(|line| line.contains("name=\""))
98            .ok_or_else(|| CurrentDistroboxError::Name(CONTAINER_ENV.to_string()))?
99            .trim_start_matches("name=\"")
100            .trim_end_matches('"');
101
102        Ok(Self {
103            name: name.to_string(),
104        })
105    }
106}
107
108#[async_trait]
109impl environment::IsEnvironment for Distrobox {
110    type Err = Error;
111
112    async fn exists(&self) -> bool {
113        if detect() {
114            true
115        } else if let Environment::Host(host) = environment::current() {
116            // The result in this case is indeed `Infallible`, but switching an `if-let` for an
117            // `unwrap` is outright stupid IMO.
118            #[allow(irrefutable_let_patterns)]
119            if let Ok(mut cmd) = host
120                .execute(crate::environment::cmd!("distrobox", "--version"))
121                .await
122            {
123                cmd.stdout(std::process::Stdio::null())
124                    .stderr(std::process::Stdio::null())
125                    .status()
126                    .await
127                    .map(|status| status.success())
128                    .unwrap_or(false)
129            } else {
130                false
131            }
132        } else {
133            false
134        }
135    }
136
137    async fn execute(&self, command: CommandLine) -> Result<Command, Self::Err> {
138        debug!("preparing execution: {}", command);
139        let mut cmd: Command;
140
141        match environment::current() {
142            Environment::Distrobox(t) => {
143                if self == &t {
144                    // This is the toolbx container we are currently running in
145                    // We expect toolbx containers to *always* run a unix OS, or at least something
146                    // that has `sudo`.
147                    if command.get_privileged() {
148                        cmd = Command::new("sudo");
149                        if !command.get_interactive() {
150                            cmd.arg("-n");
151                        }
152
153                        cmd.arg(command.command());
154                    } else {
155                        cmd = Command::new(command.command());
156                    }
157
158                    cmd.args(command.args());
159                } else {
160                    return Err(Error::Unimplemented(
161                        "running in a distrobox from another distrobox".to_string(),
162                    ));
163                }
164            }
165            Environment::Toolbx(_) => {
166                return Err(Error::Unimplemented(
167                    "running in a distrobox from a toolbx".to_string(),
168                ));
169            }
170            Environment::Host(_) => {
171                cmd = Command::new("distrobox");
172                cmd.args(["enter", "--name", &self.name]);
173
174                // Only attach to the tty if we really have a tty, too
175                // FIXME: Is this really the correct way to check?
176                if std::io::stdout().is_terminal() && std::io::stdin().is_terminal() {
177                    cmd.arg("-T");
178                }
179                cmd.arg("--");
180
181                // This is the real command we're looking for (with arguments)
182                if command.get_privileged() {
183                    cmd.args(["sudo", "-S", "-E"]);
184                }
185
186                cmd.arg(command.command()).args(command.args());
187            }
188            #[cfg(test)]
189            Environment::Mock(_) => unimplemented!(),
190        }
191
192        trace!("full command: {:?}", cmd);
193        Ok(cmd)
194    }
195}
196
197/// Detect if the current execution environment is a Toolbx container.
198///
199/// Checks for the presence of the `.toolboxenv` files.
200pub fn detect() -> bool {
201    Path::new(DISTROBOX_ENV).exists()
202}
203
204#[derive(Debug, ThisError)]
205pub enum StartDistroboxError {
206    #[error("working with distrobox containers requires the 'podman' executable")]
207    NeedPodman,
208
209    #[error("podman exited with non-zero code: {0:#?}")]
210    Podman(std::process::Output),
211
212    #[error("no distrobox with name {0} exists")]
213    NonExistent(String),
214
215    #[error("unknown I/O error occured")]
216    IoError(#[from] std::io::Error),
217}
218
219#[derive(Debug, ThisError)]
220pub enum NewDistroboxError {
221    #[error("failed to determine default distrobox name")]
222    UnknownDefault(#[from] DefaultToolbxError),
223
224    #[error("failed to start distrobox container with name '{name}'")]
225    CannotStart {
226        source: StartDistroboxError,
227        name: String,
228    },
229}
230
231#[derive(Debug, ThisError)]
232pub enum CurrentDistroboxError {
233    #[error("cannot read toolbx info from environment file '{env_file}'")]
234    Environment {
235        env_file: String,
236        source: std::io::Error,
237    },
238
239    #[error("program currently isn't run from a toolbx")]
240    NotAToolbx,
241
242    #[error("failed to read toolbx name from environment file '{0}'")]
243    Name(String),
244}
245
246#[derive(Debug, ThisError)]
247pub enum DefaultToolbxError {
248    #[error("failed to read OS information from '{file}'")]
249    UnknownOs {
250        file: String,
251        source: std::io::Error,
252    },
253
254    #[error("cannot determine OS ID from os-release info")]
255    Id,
256
257    #[error("cannot determine OS VERSION_ID from os-release info")]
258    VersionId,
259}
260
261#[derive(Debug, ThisError)]
262pub enum Error {
263    #[error("cannot determine current working directory")]
264    UnknownCwd(#[from] std::io::Error),
265
266    #[error("not implemented: {0}")]
267    Unimplemented(String),
268}