Skip to main content

cnf_lib/environment/
distrobox.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// SPDX-FileCopyrightText: (C) 2023 Andreas Hartmann <hartan@7x.de>
3// This file is part of cnf-lib, available at <https://gitlab.com/hartang/rust/cnf>
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/// Environment for distrobox containers.
20#[derive(PartialEq, Eq, Debug, PartialOrd, Ord, Serialize, Deserialize)]
21pub struct Distrobox {
22    name: String,
23}
24
25impl fmt::Display for Distrobox {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        write!(f, "distrobox '{}'", self.name)
28    }
29}
30
31impl Distrobox {
32    /// Start a Distrobox container with a given name.
33    ///
34    /// Checks if the Distrobox container exists and starts it, if necessary. If `None` is given as
35    /// name, will fall back to the default Distrobox name and start that instead. Returns an error
36    /// if unsuccessful.
37    pub fn new(name: Option<String>) -> Result<Self, NewDistroboxError> {
38        let name = match name {
39            Some(name) if !name.is_empty() => name,
40            _ => "my-distrobox".to_string(),
41        };
42
43        // Do an optimistic start:
44        // - If the container exists and isn't started, it will be started
45        // - If the container exists and is started, nothing happens
46        // - If the container doesn't exist, we get an error and report that
47        let ret = Self { name: name.clone() };
48        ret.start()
49            .map_err(|e| NewDistroboxError::CannotStart { source: e, name })?;
50        Ok(ret)
51    }
52
53    /// Starts a given Distrobox container.
54    ///
55    /// This function is automatically called by `new()` above and should only ever be called when
56    /// creating a `Distrobox` object without using the constructor. This is currently the case
57    /// when executing aliases in `cnf`, as the `Distrobox` instance is deserialized from the
58    /// config in that case.
59    pub fn start(&self) -> Result<(), StartDistroboxError> {
60        let output = std::process::Command::new("podman")
61            .args(["start", &self.name])
62            .output()
63            .map_err(|e| match e.kind() {
64                std::io::ErrorKind::NotFound => StartDistroboxError::NeedPodman,
65                _ => StartDistroboxError::IoError(e),
66            })?;
67        if output.status.success() {
68            // All good
69            Ok(())
70        } else {
71            let matcher = OutputMatcher::new(&output);
72            if matcher.starts_with("Error: no container with name or ID")
73                && matcher.contains("found: no such container")
74            {
75                Err(StartDistroboxError::NonExistent(self.name.clone()))
76            } else {
77                Err(StartDistroboxError::Podman(output))
78            }
79        }
80    }
81
82    /// Get the Toolbx container currently executing CNF.
83    ///
84    /// Will return an error if the current execution environment isn't Toolbx.
85    pub fn current() -> Result<Self, CurrentDistroboxError> {
86        if !detect() {
87            return Err(CurrentDistroboxError::NotAToolbx);
88        }
89
90        let content = std::fs::read_to_string(CONTAINER_ENV).map_err(|e| {
91            CurrentDistroboxError::Environment {
92                env_file: CONTAINER_ENV.to_string(),
93                source: e,
94            }
95        })?;
96        let name = content
97            .lines()
98            .find(|line| line.contains("name=\""))
99            .ok_or_else(|| CurrentDistroboxError::Name(CONTAINER_ENV.to_string()))?
100            .trim_start_matches("name=\"")
101            .trim_end_matches('"');
102
103        Ok(Self {
104            name: name.to_string(),
105        })
106    }
107}
108
109#[async_trait]
110impl environment::IsEnvironment for Distrobox {
111    type Err = Error;
112
113    async fn exists(&self) -> bool {
114        if detect() {
115            true
116        } else if let Environment::Host(host) = environment::current() {
117            // The result in this case is indeed `Infallible`, but switching an `if-let` for an
118            // `unwrap` is outright stupid in my opinion.
119            #[allow(irrefutable_let_patterns)]
120            if let Ok(mut cmd) = host
121                .execute(crate::environment::cmd!("distrobox", "--version"))
122                .await
123            {
124                cmd.stdout(std::process::Stdio::null())
125                    .stderr(std::process::Stdio::null())
126                    .status()
127                    .await
128                    .map(|status| status.success())
129                    .unwrap_or(false)
130            } else {
131                false
132            }
133        } else {
134            false
135        }
136    }
137
138    async fn execute(&self, command: CommandLine) -> Result<Command, Self::Err> {
139        debug!("preparing execution: {}", command);
140        let mut cmd: Command;
141
142        match environment::current() {
143            Environment::Distrobox(t) => {
144                if self == &t {
145                    // This is the Toolbx container we are currently running in
146                    // We expect Toolbx containers to *always* run a unix OS, or at least something
147                    // that has `sudo`.
148                    if command.get_privileged() {
149                        cmd = Command::new("sudo");
150                        if !command.get_interactive() {
151                            cmd.arg("-n");
152                        }
153
154                        cmd.arg(command.command());
155                    } else {
156                        cmd = Command::new(command.command());
157                    }
158
159                    cmd.args(command.args());
160                } else {
161                    return Err(Error::Unimplemented(
162                        "running in a distrobox from another distrobox".to_string(),
163                    ));
164                }
165            }
166            Environment::Toolbx(_) => {
167                return Err(Error::Unimplemented(
168                    "running in a distrobox from a toolbx".to_string(),
169                ));
170            }
171            Environment::Host(_) => {
172                cmd = Command::new("distrobox");
173                cmd.args(["enter", "--name", &self.name]);
174
175                // Only attach to the TTY if we really have a TTY, too
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/// Errors related to starting concrete Distrobox instances.
205#[derive(Debug, ThisError, Display)]
206pub enum StartDistroboxError {
207    /// working with distrobox containers requires the 'podman' executable
208    NeedPodman,
209
210    /// podman exited with non-zero code: {0:#?}
211    Podman(std::process::Output),
212
213    /// no distrobox with name {0} exists
214    NonExistent(String),
215
216    /// unknown I/O error occured
217    IoError(#[from] std::io::Error),
218}
219
220/// Errors related to starting a named distrobox instance.
221#[derive(Debug, ThisError, Display)]
222pub enum NewDistroboxError {
223    /// failed to determine default distrobox name
224    UnknownDefault(#[from] DefaultToolbxError),
225
226    /// failed to start distrobox container with name '{name}': {source}
227    CannotStart {
228        /// Underlying error source.
229        source: StartDistroboxError,
230        /// Name of the distrobox that failed to start.
231        name: String,
232    },
233}
234
235/// Errors related to distrobox as environment that launched `cnf`.
236#[derive(Debug, ThisError, Display)]
237pub enum CurrentDistroboxError {
238    /// cannot read distrobox info from environment file '{env_file}': {source}
239    Environment {
240        /// environment file that couldn't be read.
241        env_file: String,
242        /// Error from trying to read the environment file.
243        source: std::io::Error,
244    },
245
246    /// program currently isn't run from a toolbx
247    NotAToolbx,
248
249    /// failed to read distrobox name from environment file '{0}'
250    Name(String),
251}
252
253/// Errors related to the configured default distrobox container.
254#[derive(Debug, ThisError, Display)]
255pub enum DefaultToolbxError {
256    /// failed to read OS information from '{file}': {source}
257    UnknownOs {
258        /// File that couldn't be read.
259        file: String,
260        /// Error from trying to read the file.
261        source: std::io::Error,
262    },
263
264    /// cannot determine OS ID from os-release info
265    Id,
266
267    /// cannot determine OS VERSION_ID from os-release info
268    VersionId,
269}
270
271/// Error type for environment impl.
272#[derive(Debug, ThisError, Display)]
273pub enum Error {
274    /// cannot determine current working directory
275    UnknownCwd(#[from] std::io::Error),
276
277    /// not implemented: {0}
278    Unimplemented(String),
279}