Skip to main content

conmonrs/
config.rs

1//! Configuration related structures.
2use crate::{fs_util, listener, process::is_zombie};
3use anyhow::{Context, Result, bail};
4use clap::{Parser, ValueEnum};
5use nix::{
6    errno::Errno,
7    sys::{
8        signal::kill,
9        socket::{
10            AddressFamily, SockFlag, SockType, UnixAddr, connect, getsockopt, socket,
11            sockopt::PeerCredentials,
12        },
13    },
14    unistd::Pid,
15};
16use std::{
17    ffi::OsStr,
18    fmt, fs,
19    os::fd::{AsFd, AsRawFd},
20    path::{self, Path, PathBuf},
21};
22use tracing_subscriber::filter::LevelFilter;
23
24macro_rules! prefix {
25    () => {
26        "CONMON_"
27    };
28}
29
30#[derive(Clone, Debug, Eq, Parser, PartialEq)]
31#[command(
32    after_help("More info at: https://github.com/containers/conmon-rs"),
33    disable_version_flag(true)
34)]
35/// A pod level OCI container runtime monitor, started by a container engine like CRI-O.
36pub(crate) struct Config {
37    #[arg(
38        default_missing_value("default"),
39        env(concat!(prefix!(), "VERSION")),
40        long("version"),
41        num_args(0..=1),
42        short('v'),
43        value_enum,
44        value_name("VERBOSITY")
45    )]
46    /// Show version information, specify "full" for verbose output.
47    version: Option<Verbosity>,
48
49    #[arg(
50        default_missing_value("default"),
51        env(concat!(prefix!(), "VERSION_JSON")),
52        long("version-json"),
53        visible_alias("vj"),
54        short('j'),
55        num_args(0..=1),
56        value_enum,
57        value_name("VERBOSITY")
58    )]
59    /// Show version information as parsable JSON, specify "full" for verbose output.
60    version_json: Option<Verbosity>,
61
62    #[arg(
63        default_value_t,
64        env(concat!(prefix!(), "LOG_LEVEL")),
65        long("log-level"),
66        short('l'),
67        value_enum,
68        value_name("LEVEL")
69    )]
70    /// The logging level of the server.
71    log_level: LogLevel,
72
73    #[arg(
74        default_value_t,
75        env(concat!(prefix!(), "LOG_DRIVER")),
76        long("log-driver"),
77        short('d'),
78        value_enum,
79        value_name("DRIVER")
80    )]
81    /// The logging driver used by the server.
82    log_driver: LogDriver,
83
84    // The blank default lets `--version` work without the flag, see
85    // `Config::validate`.
86    #[arg(
87        default_value(" "),
88        env(concat!(prefix!(), "RUNTIME")),
89        hide_default_value(true),
90        long("runtime"),
91        short('r'),
92        value_name("RUNTIME")
93    )]
94    /// Binary path of the OCI runtime to use to operate on the containers.
95    /// Required to run the server.
96    runtime: PathBuf,
97
98    #[arg(
99        default_value(" "),
100        env(concat!(prefix!(), "RUNTIME_DIR")),
101        hide_default_value(true),
102        long("runtime-dir"),
103        value_name("RUNTIME_DIR")
104    )]
105    /// Path of the directory for conmonrs to hold files at runtime. Required
106    /// to run the server.
107    runtime_dir: PathBuf,
108
109    #[arg(
110        env(concat!(prefix!(), "RUNTIME_ROOT")),
111        long("runtime-root"),
112        value_name("RUNTIME_ROOT")
113    )]
114    /// Root directory used by the OCI runtime to operate on containers, passed to it as
115    /// `--root`.
116    runtime_root: Option<PathBuf>,
117
118    #[arg(
119        env(concat!(prefix!(), "SKIP_FORK")),
120        long("skip-fork"),
121        // The environment variable also accepts 1, 0, yes and no.
122        value_parser = clap::builder::BoolishValueParser::new(),
123    )]
124    /// Stay in the foreground instead of forking into the background.
125    skip_fork: bool,
126
127    #[arg(
128        env(concat!(prefix!(), "ENABLE_TRACING")),
129        long("enable-tracing"),
130        short('e'),
131        // Like `skip_fork`.
132        value_parser = clap::builder::BoolishValueParser::new(),
133    )]
134    /// Enable OpenTelemetry tracing. Builds without the `tracing` feature
135    /// ignore it with a warning.
136    enable_tracing: bool,
137
138    #[arg(
139        default_value("http://127.0.0.1:4317"),
140        env(concat!(prefix!(), "TRACING_ENDPOINT")),
141        long("tracing-endpoint"),
142        short('t'),
143        value_name("URL")
144    )]
145    /// OpenTelemetry gRPC endpoint to be used for tracing. Builds without the
146    /// `tracing` feature ignore it.
147    tracing_endpoint: String,
148}
149
150impl Config {
151    /// Show version information.
152    pub(crate) fn version(&self) -> Option<Verbosity> {
153        self.version
154    }
155
156    /// Show version information as JSON.
157    pub(crate) fn version_json(&self) -> Option<Verbosity> {
158        self.version_json
159    }
160
161    /// The logging level.
162    pub(crate) fn log_level(&self) -> LogLevel {
163        self.log_level
164    }
165
166    /// The logging driver.
167    pub(crate) fn log_driver(&self) -> LogDriver {
168        self.log_driver
169    }
170
171    /// Binary path of the OCI runtime.
172    pub(crate) fn runtime(&self) -> &Path {
173        &self.runtime
174    }
175
176    /// Whether the runtime is crun, which gets arguments of its own, see
177    /// `GenerateRuntimeArgs::default_args`.
178    pub(crate) fn runtime_is_crun(&self) -> bool {
179        self.runtime.file_name() == Some(OsStr::new("crun"))
180    }
181
182    /// Path of the directory for conmonrs to hold files at runtime.
183    pub(crate) fn runtime_dir(&self) -> &Path {
184        &self.runtime_dir
185    }
186
187    /// Root directory used by the OCI runtime.
188    pub(crate) fn runtime_root(&self) -> Option<&Path> {
189        self.runtime_root.as_deref()
190    }
191
192    /// Stay in the foreground instead of forking into the background.
193    pub(crate) fn skip_fork(&self) -> bool {
194        self.skip_fork
195    }
196
197    /// Enable OpenTelemetry tracing.
198    pub(crate) fn enable_tracing(&self) -> bool {
199        self.enable_tracing
200    }
201
202    /// OpenTelemetry gRPC endpoint.
203    #[cfg(feature = "tracing")]
204    pub(crate) fn tracing_endpoint(&self) -> &str {
205        &self.tracing_endpoint
206    }
207}
208
209/// Available log levels.
210#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
211pub(crate) enum LogLevel {
212    /// Trace level, the most verbose one.
213    Trace,
214
215    /// Debug level, less verbose than trace.
216    Debug,
217
218    #[default]
219    /// Info level, less verbose than debug.
220    Info,
221
222    /// Warn level, less verbose than info.
223    Warn,
224
225    /// Error level, showing only errors.
226    Error,
227
228    /// Disable logging.
229    Off,
230}
231
232/// Available verbosity levels.
233#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
234pub(crate) enum Verbosity {
235    /// The default output verbosity.
236    Default,
237
238    /// The full output verbosity.
239    Full,
240}
241
242/// Available log drivers.
243#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
244pub(crate) enum LogDriver {
245    /// Use no log driver.
246    None,
247
248    /// Use stdout as log driver.
249    Stdout,
250
251    #[default]
252    /// Use systemd journald as log driver.
253    Systemd,
254}
255
256/// Formats the level with the name clap derives from the variant, which is
257/// what the command line and the runtime flags use.
258impl fmt::Display for LogLevel {
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260        let value = self
261            .to_possible_value()
262            .expect("LogLevel has no skipped variants");
263        f.write_str(value.get_name())
264    }
265}
266
267impl From<LogLevel> for LevelFilter {
268    fn from(level: LogLevel) -> Self {
269        match level {
270            LogLevel::Trace => Self::TRACE,
271            LogLevel::Debug => Self::DEBUG,
272            LogLevel::Info => Self::INFO,
273            LogLevel::Warn => Self::WARN,
274            LogLevel::Error => Self::ERROR,
275            LogLevel::Off => Self::OFF,
276        }
277    }
278}
279
280impl Config {
281    /// Parse the configuration from the command line and the environment.
282    pub(crate) fn parse_args() -> Self {
283        Self::parse()
284    }
285}
286
287// The socket and the PID file are the ones of `pkg/client/client.go`.
288const SOCKET: &str = "conmon.sock";
289const PIDFILE: &str = "pidfile";
290
291/// The fd socket of older servers, see [`Config::prepare`].
292const OLD_FD_SOCKET: &str = "conmon-fd.sock";
293
294// The flags which the errors of `Config::validate` name.
295const RUNTIME_FLAG: &str = "--runtime";
296const RUNTIME_DIR_FLAG: &str = "--runtime-dir";
297const RUNTIME_ROOT_FLAG: &str = "--runtime-root";
298
299/// Fail for a runtime flag which is blank or not UTF-8. The runtime flags
300/// default to a blank, so that `--version` works without them.
301fn require_set(flag: &str, path: &Path) -> Result<()> {
302    if path
303        .to_str()
304        .with_context(|| format!("{flag} does not parse as string"))?
305        .trim()
306        .is_empty()
307    {
308        bail!("{flag} flag not set")
309    }
310    Ok(())
311}
312
313impl Config {
314    /// Validate the configuration integrity.
315    pub(crate) fn validate(&self) -> Result<()> {
316        require_set(RUNTIME_FLAG, self.runtime())?;
317        require_set(RUNTIME_DIR_FLAG, self.runtime_dir())?;
318
319        // The file name tells whether the runtime is crun, a path like "/"
320        // would only fail every runtime call later.
321        if self.runtime().file_name().is_none() {
322            bail!(
323                "{} '{}' has no file name",
324                RUNTIME_FLAG,
325                self.runtime().display()
326            )
327        }
328
329        if !self.runtime().exists() {
330            bail!(
331                "{} '{}' does not exist",
332                RUNTIME_FLAG,
333                self.runtime().display()
334            )
335        }
336
337        if let Some(rr) = self.runtime_root()
338            && rr.exists()
339            && !rr.is_dir()
340        {
341            bail!(
342                "{} '{}' is not a directory",
343                RUNTIME_ROOT_FLAG,
344                rr.display()
345            )
346        }
347
348        Ok(())
349    }
350
351    /// Make the paths absolute, the server changes into the root directory
352    /// once it started, see `Server::start`.
353    pub(crate) fn make_absolute(&mut self) -> Result<()> {
354        self.runtime = path::absolute(&self.runtime).context("make runtime path absolute")?;
355        self.runtime_dir =
356            path::absolute(&self.runtime_dir).context("make runtime dir absolute")?;
357        if let Some(rr) = &mut self.runtime_root {
358            *rr = path::absolute(&*rr).context("make runtime root absolute")?;
359        }
360        Ok(())
361    }
362
363    /// Create the runtime directories and remove stale sockets. Separate from
364    /// [`Config::validate`], which only checks and changes nothing on disk.
365    pub(crate) fn prepare(&self) -> Result<()> {
366        fs::create_dir_all(self.runtime_dir()).context("create runtime dir")?;
367
368        if let Some(rr) = self.runtime_root() {
369            fs::create_dir_all(rr).context("create runtime root")?;
370        }
371
372        // A socket left behind gets replaced, but not the one of a server
373        // which still runs, its containers would be lost otherwise. A socket
374        // which cannot be checked may be in use as well.
375        let socket = self.socket();
376        if Self::socket_in_use(&socket)
377            .with_context(|| format!("check socket {}", socket.display()))?
378        {
379            bail!(listener::SocketInUse(format!(
380                "socket {} is in use by a running server",
381                socket.display()
382            )))
383        }
384        fs_util::remove_file(&socket)
385            .with_context(|| format!("remove stale socket {}", socket.display()))?;
386
387        // The fd socket of an older server in a reused runtime directory,
388        // which nothing serves anymore. Only a leftover, which must not stop
389        // the server, the logging does not run yet, like for `init`.
390        let fd_socket = self.runtime_dir().join(OLD_FD_SOCKET);
391        if let Err(e) = fs_util::remove_file(&fd_socket) {
392            eprintln!(
393                "Unable to remove the stale socket {}: {e:#}",
394                fd_socket.display()
395            );
396        }
397
398        Ok(())
399    }
400
401    /// Whether a server accepts connections on the socket. The connect does
402    /// not wait for a server which does not accept them in time, a full
403    /// backlog counts as in use as well. A socket of a server which exited
404    /// is not in use, also if a process left behind still holds it.
405    fn socket_in_use(path: &Path) -> Result<bool> {
406        // The path may be too long for a socket address.
407        let (short_path, _dir) = listener::shorten_path(path)?;
408        let fd = socket(
409            AddressFamily::Unix,
410            SockType::Stream,
411            SockFlag::SOCK_CLOEXEC | SockFlag::SOCK_NONBLOCK,
412            None,
413        )
414        .context("create socket")?;
415        let addr = UnixAddr::new(&short_path).context("create socket address")?;
416        Ok(match connect(fd.as_raw_fd(), &addr) {
417            Ok(()) => Self::peer_running(&fd),
418            Err(Errno::EAGAIN) => true,
419            // Nothing which this server could reach serves it: the socket
420            // is gone, left behind or something else.
421            Err(Errno::ECONNREFUSED | Errno::ENOENT | Errno::ENOTSOCK | Errno::EPROTOTYPE) => false,
422            // Like one of another user left behind, removing it only needs
423            // the permissions of the directory. The logging is not set up yet,
424            // so it goes to stderr.
425            Err(Errno::EACCES) => {
426                eprintln!(
427                    "Replacing socket {} which is not accessible",
428                    path.display()
429                );
430                false
431            }
432            // Running out of resources tells nothing about the socket.
433            Err(e) => return Err(e).context("connect socket"),
434        })
435    }
436
437    /// Whether the process which listens on the other end of a connection
438    /// still runs, true if that cannot be told.
439    fn peer_running(fd: &impl AsFd) -> bool {
440        let Ok(credentials) = getsockopt(fd, PeerCredentials) else {
441            return true;
442        };
443        let pid = credentials.pid();
444        // From another PID namespace.
445        if pid <= 0 {
446            return true;
447        }
448        // A zombie does not accept either.
449        let pid = Pid::from_raw(pid);
450        kill(pid, None) != Err(Errno::ESRCH) && !is_zombie(pid)
451    }
452
453    /// The socket the server serves the RPCs on.
454    pub(crate) fn socket(&self) -> PathBuf {
455        self.runtime_dir().join(SOCKET)
456    }
457
458    /// The PID file the parent writes for the forked server.
459    pub(crate) fn conmon_pidfile(&self) -> PathBuf {
460        self.runtime_dir().join(PIDFILE)
461    }
462}
463
464impl Config {
465    /// Parse a configuration from the arguments for a test. The `CONMON_*`
466    /// variables of the environment would change what the parsed
467    /// configuration contains, so they get ignored.
468    #[cfg(test)]
469    pub(crate) fn parse_for_test<I, T>(args: I) -> Self
470    where
471        I: IntoIterator<Item = T>,
472        T: Into<std::ffi::OsString> + Clone,
473    {
474        use clap::{CommandFactory, FromArgMatches};
475        let without_env = |arg: clap::Arg| arg.env(None);
476        let matches = <Self as CommandFactory>::command()
477            .mut_args(without_env)
478            .try_get_matches_from(
479                std::iter::once("conmonrs".into()).chain(args.into_iter().map(Into::into)),
480            )
481            .expect("parse args");
482        Self::from_arg_matches(&matches).expect("parse args")
483    }
484}
485
486#[cfg(test)]
487mod tests;