saluki_env/features/
mod.rs

1//! Feature detection.
2//!
3//! This module provides helpers for detecting the presence of various "features" in the environment, such as if
4//! containerd is running, and so on. Feature detection is useful for knowing what capabilities are available, and what
5//! code should or shouldn't be run.
6use std::path::{Path, PathBuf};
7#[cfg(unix)]
8use std::{io::ErrorKind, net::Shutdown, os::unix::fs::FileTypeExt as _, time::Duration};
9
10use bitmask_enum::bitmask;
11#[cfg(unix)]
12use socket2::{Domain, SockAddr, Socket, Type};
13use tracing::debug;
14
15mod containerd;
16pub use self::containerd::ContainerdDetector;
17
18mod detector;
19pub use self::detector::FeatureDetector;
20
21const CONTAINER_HOST_MOUNT_PATH: &str = "/host";
22#[cfg(unix)]
23const SOCKET_CHECK_CONNECT_TIMEOUT: Duration = Duration::from_millis(500);
24
25/// Features are distinct markers or indicators of a particular technology or platform that's present.
26///
27/// In general, features represent the type of environment that the application is running in, such as the Kubernetes
28/// feature being present indicating that the application is running in Kubernetes, and so on.
29#[bitmask(u16)]
30#[bitmask_config(vec_debug)]
31pub enum Feature {
32    /// Host-mapped procfs.
33    ///
34    /// This implies that we're in a containerized environment and the host's procfs (`/proc`) has been mapped into the
35    /// container using a `/host` prefix, resulting in a `/host/proc` path.
36    HostMappedProcfs,
37
38    /// Host-mapped cgroupfs.
39    ///
40    /// This implies that we're in a containerized environment and the host's cgroupfs (`/sys/fs/cgroup`) has been
41    /// mapped into the container using a `/host` prefix, resulting in a `/host/sys/fs/cgroup` path.
42    HostMappedCgroupfs,
43
44    /// Containerd.
45    Containerd,
46}
47
48fn get_env_var_or_empty(name: &str) -> String {
49    std::env::var(name).unwrap_or_else(|_| String::new())
50}
51
52fn is_env_var_present(name: &str) -> bool {
53    !get_env_var_or_empty(name).is_empty()
54}
55
56/// Returns whether the process runs in an Amazon ECS Fargate environment.
57pub fn is_ecs_fargate() -> bool {
58    is_env_var_present("ECS_FARGATE") || get_env_var_or_empty("AWS_EXECUTION_ENV") == "AWS_ECS_FARGATE"
59}
60
61fn file_exists<P>(path: P) -> bool
62where
63    P: AsRef<Path>,
64{
65    std::fs::metadata(path).is_ok()
66}
67
68#[cfg(unix)]
69fn path_empty<P>(path: P) -> bool
70where
71    P: AsRef<Path>,
72{
73    path.as_ref().as_os_str().is_empty()
74}
75
76#[cfg(unix)]
77fn path_contains<P>(path: P, fragment: &str) -> bool
78where
79    P: AsRef<Path>,
80{
81    path.as_ref().to_string_lossy().contains(fragment)
82}
83
84/// Checks to see if a Unix domain sockets exists, and is reachable, at the given path.
85///
86/// If there is no file at the given path, or if the file isn't of the socket type, `None` is returned. If the socket
87/// at the given path isn't reachable, `Some(false)` is returned. Otherwise, `Some(true)` is returned.
88///
89/// Reachability is determined as either being able to connect or having the permissions to do so. We do this because it
90/// is likely that if no process is _currently_ listening on the socket, one will likely be in the future.
91#[cfg(unix)]
92fn check_unix_socket(path: &Path) -> Option<bool> {
93    // Make sure the path exists and is a socket.
94    let metadata = match std::fs::metadata(path) {
95        Ok(metadata) => metadata,
96        Err(e) => {
97            debug!(socket_path = %path.to_string_lossy(), error = %e, "Failed to get metadata for socket path.");
98            return None;
99        }
100    };
101    if !metadata.file_type().is_socket() {
102        return None;
103    }
104
105    // Check to see if we can connect to the socket.
106    //
107    // We treat every error other than `PermissionDenied` as a non-failure, with the thought being that if the file
108    // exists and is a socket, the process is potentially not listening _yet_, but likely will in the future.
109    let socket = match Socket::new_raw(Domain::UNIX, Type::STREAM, None) {
110        Ok(socket) => socket,
111        Err(e) => {
112            debug!(socket_path = %path.to_string_lossy(), error = %e, "Failed to create socket.");
113            return Some(false);
114        }
115    };
116
117    let socket_addr = match SockAddr::unix(path) {
118        Ok(socket_addr) => socket_addr,
119        Err(e) => {
120            debug!(socket_path = %path.to_string_lossy(), error = %e, "Failed to create socket address.");
121            return Some(false);
122        }
123    };
124
125    Some(
126        match socket.connect_timeout(&socket_addr, SOCKET_CHECK_CONNECT_TIMEOUT) {
127            Ok(_) => {
128                // Shutdown the socket, ignoring any errors.
129                let _ = socket.shutdown(Shutdown::Both);
130
131                true
132            }
133            Err(e) => e.kind() != ErrorKind::PermissionDenied,
134        },
135    )
136}
137
138#[cfg(unix)]
139fn find_first_available_unix_socket<I, P>(socket_paths: I) -> Option<PathBuf>
140where
141    I: IntoIterator<Item = P>,
142    P: AsRef<Path>,
143{
144    for socket_path in socket_paths {
145        let socket_path = socket_path.as_ref();
146        let socket_path_str = socket_path.to_string_lossy();
147
148        match check_unix_socket(socket_path) {
149            None => {
150                debug!(socket_path = %socket_path_str, "No file at socket path or not a socket.");
151                continue;
152            }
153            Some(false) => {
154                debug!(socket_path = %socket_path_str, "Found file at socket path but unreachable. (permissions?)");
155                continue;
156            }
157            Some(true) => {
158                debug!(socket_path = %socket_path_str, "Found reachable socket.");
159                return Some(socket_path.to_path_buf());
160            }
161        }
162    }
163
164    None
165}
166
167fn is_running_inside_container() -> bool {
168    // `DOCKER_DD_AGENT` is set by the official Datadog Agent container image.
169    let is_containerized = is_env_var_present("DOCKER_DD_AGENT");
170    if is_containerized {
171        debug!("Found non-empty DOCKER_DD_AGENT environment variable. Likely running in a container.");
172    } else {
173        debug!("Did not find DOCKER_DD_AGENT environment variable. Likely not running in a container.");
174    }
175    is_containerized
176}
177
178#[cfg(unix)]
179fn is_running_inside_docker() -> bool {
180    // This file is mounted into a container's filesystem by Docker itself, and implies that we're currently _inside_
181    // the Docker runtime. `is_docker_present` detects the presence of the Docker runtime on the host itself, at the OS
182    // level.
183    file_exists("/.dockerenv")
184}
185
186#[cfg(unix)]
187fn with_host_mount_prefixes<I, P>(paths: I) -> Vec<PathBuf>
188where
189    I: IntoIterator<Item = P>,
190    P: AsRef<str>,
191{
192    let mut prefixes = vec![];
193
194    // Add the provided paths as they are, joined with a leading slash, which will absolute-ize them if they are
195    // relative. If we're in a containerized environment, we'll also add a "/host"-anchored version of each path.
196    //
197    // We do this to avoid clobbering paths in the container, such that if we mount a host path into the container, such
198    // as "/var/run", it ends up as "/host/var/run" in the container, which doesn't shadow the existing "/var/run".
199    let root = PathBuf::from("/");
200    let host_root = PathBuf::from(CONTAINER_HOST_MOUNT_PATH);
201    let is_containerized = is_running_inside_container();
202
203    for path in paths {
204        let path = path.as_ref().trim_start_matches('/');
205
206        prefixes.push(root.join(path));
207
208        if is_containerized {
209            prefixes.push(host_root.join(path));
210        }
211    }
212
213    prefixes
214}
215
216fn has_host_mapped_procfs() -> bool {
217    let path = PathBuf::from(CONTAINER_HOST_MOUNT_PATH).join("proc");
218    is_running_inside_container() && file_exists(&path)
219}
220
221fn has_host_mapped_cgroupfs() -> bool {
222    let path = PathBuf::from(CONTAINER_HOST_MOUNT_PATH).join("sys/fs/cgroup");
223    is_running_inside_container() && file_exists(&path)
224}