Skip to main content

cnf_lib/environment/
mod.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//! # Environment handlers
6//!
7//! This module contains handlers for various [environments](Environment). Generally a handler is
8//! characterized by implementing the [`IsEnvironment`] trait and integrating with the [`current`]
9//! function.
10//!
11//! Especially [`IsEnvironment::execute`] function isn't usually trivial to implement, since the
12//! precise commands to run (and wrap) depend on the *current* and *target* execution environment.
13pub mod container;
14pub mod distrobox;
15pub mod host;
16pub mod toolbx;
17#[cfg(test)]
18use crate::test::prelude::*;
19use futures::TryFutureExt;
20use prelude::*;
21use tracing::Instrument;
22
23#[allow(unused_imports)]
24pub(crate) mod prelude {
25    pub(crate) use crate::{
26        environment::{self, Environment},
27        util::{CommandLine, OutputMatcher, cmd},
28    };
29    pub(crate) use async_trait::async_trait;
30    pub(crate) use displaydoc::Display;
31    pub(crate) use serde_derive::{Deserialize, Serialize};
32    pub(crate) use std::fmt;
33    pub(crate) use thiserror::Error as ThisError;
34    pub(crate) use tokio::process::Command;
35    pub(crate) use tracing::{debug, error, info, span, trace, warn};
36}
37
38/// Trait for usable environments.
39#[async_trait]
40pub trait IsEnvironment: fmt::Debug + fmt::Display {
41    /// Error produced by this environment impl.
42    type Err;
43
44    /// Returns true if the given Env is available at all.
45    ///
46    /// We assume the environment `Host` to be available unconditionally. Other environments, such
47    /// as `toolbx`, can only be available when at least the `toolbx` executable is present, or we
48    /// are currently inside a toolbx.
49    async fn exists(&self) -> bool;
50
51    /// Execute a command within this environment
52    ///
53    /// [`IsProvider`](crate::provider::IsProvider) implementations should prefer calling
54    /// [`output_of()`] instead of interacting with an [`Environment`] instance directly.
55    /// Refer to [`output_of()`] for details.
56    ///
57    /// [`output_of()`]: Environment::output_of()
58    async fn execute(&self, command: CommandLine) -> Result<Command, Self::Err>;
59}
60
61/// All the execution environments known to the applicaiton.
62#[derive(PartialEq, Eq, Debug, PartialOrd, Ord, Serialize, Deserialize)]
63pub enum Environment {
64    //Container,
65    /// The host system
66    Host(host::Host),
67    /// A distrobox instance
68    Distrobox(distrobox::Distrobox),
69    /// A toolbx instance
70    Toolbx(toolbx::Toolbx),
71    /// Mock environment (test only)
72    #[cfg(test)]
73    Mock(Mock),
74}
75
76impl Environment {
77    /// Execute a command in this environment and collect stdout, if possible.
78    ///
79    /// Executes `cmd` inside this env, collecting all output. If execution finishes successfully,
80    /// the stdout is returned as [`String`]. Otherwise, an [`ExecutionError`] is returned instead.
81    ///
82    /// The [`ExecutionError`] type takes care of a lot of boilerplate code that is otherwise necessary
83    /// to detect specific error conditions caused by *different* environments (because they can
84    /// produce different output for similar/identical error conditions).
85    ///
86    /// Refer to [`OutputMatcher`] for added convenience when trying to recover from a
87    /// [`NonZero`](ExecutionError::NonZero) error.
88    ///
89    ///
90    /// ### Test integration
91    ///
92    /// In conjunction with the [`Mock`](crate::test::mock::Mock) type, this function allows
93    /// replaying the output of called commands. Use the [`quick_test!`](crate::test::quick_test)
94    /// macro to simulate command outputs inside tests.
95    #[cfg(not(test))]
96    pub async fn output_of(&self, cmd: CommandLine) -> Result<String, ExecutionError> {
97        let main_command = cmd.command();
98        let output = self
99            .execute(cmd)
100            .await
101            .map_err(ExecutionError::Environment)?
102            // NOTE: This bit is very important. If ommitted, **all** `Command` instances spawned
103            // by the application ever will block on each other waiting to read things from stdin.
104            .stdin(std::process::Stdio::null())
105            .output()
106            .await
107            .map_err(|err| match err.kind() {
108                std::io::ErrorKind::NotFound => ExecutionError::NotFound(main_command.clone()),
109                _ => ExecutionError::Unknown(err),
110            })?;
111
112        if output.status.success() {
113            Ok(String::from_utf8_lossy(&output.stdout).to_string())
114        } else {
115            let matcher = OutputMatcher::new(&output);
116            if matcher.starts_with(
117            // When calling into toolbx from host
118                &format!(
119                    "Error: crun: executable file `{}` not found in $PATH: No such file or directory",
120                    main_command.clone()
121                )
122            ) ||
123            // When calling into host from toolbx
124            matcher.starts_with(
125                    "Portal call failed: Failed to start command: Failed to execute child process"
126                ) && matcher.ends_with("(No such file or directory)")
127            {
128                Err(ExecutionError::NotFound(main_command))
129            } else {
130                Err(ExecutionError::NonZero {
131                    command: main_command,
132                    output,
133                })
134            }
135        }
136    }
137
138    /// Instrumented test version of `output_of`.
139    #[cfg(test)]
140    pub async fn output_of(&self, _command: CommandLine) -> Result<String, ExecutionError> {
141        match self {
142            Environment::Mock(mock) => mock.pop_raw(),
143            _ => panic!("cannot execute commands in tests with regular envs"),
144        }
145    }
146
147    /// Serialize an environment to a JSON string.
148    pub fn to_json(&self) -> String {
149        serde_json::to_string(&self)
150            .unwrap_or_else(|_| panic!("failed to serialize env '{}' to JSON", self))
151    }
152
153    /// Start an environment manually.
154    ///
155    /// This is usually done automatically when creating environment instances through their
156    /// constructors. For some environments, this is a no-op.
157    pub fn start(&self) -> Result<(), anyhow::Error> {
158        match self {
159            Self::Distrobox(val) => Ok(val.start()?),
160            Self::Toolbx(val) => Ok(val.start()?),
161            _ => Ok(()),
162        }
163    }
164}
165
166impl fmt::Display for Environment {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        match self {
169            Self::Host(val) => write!(f, "{}", val),
170            Self::Distrobox(val) => write!(f, "{}", val),
171            Self::Toolbx(val) => write!(f, "{}", val),
172            #[cfg(test)]
173            Self::Mock(val) => write!(f, "{}", val),
174        }
175    }
176}
177
178#[async_trait]
179impl IsEnvironment for Environment {
180    type Err = Error;
181
182    async fn exists(&self) -> bool {
183        match self {
184            Self::Host(val) => val.exists(),
185            Self::Distrobox(val) => val.exists(),
186            Self::Toolbx(val) => val.exists(),
187            #[cfg(test)]
188            Self::Mock(val) => val.exists(),
189        }
190        .await
191    }
192
193    async fn execute(&self, command: CommandLine) -> Result<Command, Self::Err> {
194        async move {
195            match self {
196                Self::Host(val) => val.execute(command).map_err(Error::ExecuteOnHost).await,
197                Self::Distrobox(val) => {
198                    val.execute(command)
199                        .map_err(|e| Self::Err::ExecuteInDistrobox {
200                            distrobox: val.to_string(),
201                            source: e,
202                        })
203                        .await
204                }
205                Self::Toolbx(val) => {
206                    val.execute(command)
207                        .map_err(|e| Self::Err::ExecuteInToolbx {
208                            toolbx: val.to_string(),
209                            source: e,
210                        })
211                        .await
212                }
213                #[cfg(test)]
214                Self::Mock(val) => Ok(val.execute(command).await.unwrap()),
215            }
216        }
217        .in_current_span()
218        .await
219    }
220}
221
222impl From<host::Host> for Environment {
223    fn from(value: host::Host) -> Self {
224        Self::Host(value)
225    }
226}
227
228impl From<distrobox::Distrobox> for Environment {
229    fn from(value: distrobox::Distrobox) -> Self {
230        Self::Distrobox(value)
231    }
232}
233
234impl From<toolbx::Toolbx> for Environment {
235    fn from(value: toolbx::Toolbx) -> Self {
236        Self::Toolbx(value)
237    }
238}
239
240impl std::str::FromStr for Environment {
241    type Err = SerializationError;
242
243    fn from_str(s: &str) -> Result<Self, Self::Err> {
244        let val: Self =
245            serde_json::from_str(s).map_err(|_| SerializationError { raw: s.to_owned() })?;
246        Ok(val)
247    }
248}
249
250/// invalid environment specification '{raw}'
251#[derive(Debug, ThisError, Serialize, Deserialize, Display)]
252pub struct SerializationError {
253    raw: String,
254}
255
256/// Return the current execution environment.
257pub fn current() -> Environment {
258    if toolbx::detect() {
259        Environment::Toolbx(toolbx::Toolbx::current().unwrap())
260    } else if distrobox::detect() {
261        Environment::Distrobox(distrobox::Distrobox::current().unwrap())
262    } else {
263        Environment::Host(host::Host::new())
264    }
265}
266
267/// Preserve the users environments.
268///
269/// Attempts to replicate all environment variables of the current process in the spawned
270/// environment. Refer to the source code to see exactly which variables are preserved. Variables
271/// are returned as a vector of strings with 'KEY=VALUE' notation.
272pub fn read_env_vars() -> Vec<String> {
273    let exclude = [
274        "HOST",
275        "HOSTNAME",
276        "HOME",
277        "LANG",
278        "LC_CTYPE",
279        "PATH",
280        "PROFILEREAD",
281        "SHELL",
282    ];
283
284    std::env::vars()
285        .filter_map(|(mut key, value)| {
286            if exclude.contains(&&key[..])
287                || key.starts_with('_')
288                || (key.starts_with("XDG_") && key.ends_with("_DIRS"))
289            {
290                None
291            } else {
292                key.push('=');
293                key.push_str(&value);
294                Some(key)
295            }
296        })
297        .collect::<Vec<_>>()
298}
299
300/// Common errors from command execution.
301#[derive(Debug, ThisError)]
302pub enum ExecutionError {
303    /// Requested executable cannot be found
304    #[error("command not found: {0}")]
305    NotFound(String),
306
307    /// Error inside an [`Environment`]
308    #[error(transparent)]
309    Environment(#[from] Error),
310
311    /// Error from [`Command`]
312    #[error(transparent)]
313    Unknown(#[from] std::io::Error),
314
315    /// Error from the called command
316    ///
317    /// ## Note
318    ///
319    /// When calling from the host into a toolbx container, *stdout* and *stderr* are both merged
320    /// into *stdout*. This is currently a shortcoming of the involved call to `podman exec -t
321    /// ...`. Refer to the manpage of `podman-exec(1)`, in particular the *NOTE* attached to
322    /// `--tty`.
323    ///
324    /// This means that when checking for messages in `output.stderr`, you should **always** check
325    /// for the existence of your message in **both stderr and stdout**.
326    #[error("command '{command}' exited with nonzero code: {output:?}")]
327    NonZero {
328        /// The command that produced the error
329        command: String,
330        /// The output associated with the error.
331        output: std::process::Output,
332    },
333}
334
335/// Error from starting up an environment.
336#[derive(Debug, ThisError)]
337pub enum StartError {
338    /// Underlying Distrobox error.
339    #[error(transparent)]
340    Distrobox(#[from] distrobox::StartDistroboxError),
341
342    /// Underlying Toolbx error.
343    #[error(transparent)]
344    Toolbx(#[from] toolbx::StartToolbxError),
345}
346
347/// Environment execution error.
348#[derive(Debug, ThisError, Display)]
349pub enum Error {
350    /// failed to execute command on host
351    ExecuteOnHost(#[from] <host::Host as IsEnvironment>::Err),
352
353    /// failed to execute command in '{toolbx}': {source}
354    ExecuteInToolbx {
355        /// Toolbx where the error originates.
356        toolbx: String,
357        /// Underlying error source.
358        source: <toolbx::Toolbx as IsEnvironment>::Err,
359    },
360
361    /// failed to execute command in '{distrobox}': {source}
362    ExecuteInDistrobox {
363        /// Distrobox where the error originates.
364        distrobox: String,
365        /// Underlying error source.
366        source: <distrobox::Distrobox as IsEnvironment>::Err,
367    },
368}