saluki_env/features/
mod.rs1use 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";
22const LEGACY_CGROUPFS_ROOT_MARKER: &str = "/cgroup/memory/memory.stat";
23#[cfg(unix)]
24const SOCKET_CHECK_CONNECT_TIMEOUT: Duration = Duration::from_millis(500);
25
26#[bitmask(u16)]
31#[bitmask_config(vec_debug)]
32pub enum Feature {
33 HostMappedProcfs,
38
39 HostMappedCgroupfs,
44
45 LegacyCgroupfsRoot,
51
52 Containerd,
54}
55
56fn get_env_var_or_empty(name: &str) -> String {
57 std::env::var(name).unwrap_or_else(|_| String::new())
58}
59
60fn is_env_var_present(name: &str) -> bool {
61 !get_env_var_or_empty(name).is_empty()
62}
63
64pub fn is_ecs_fargate() -> bool {
66 is_env_var_present("ECS_FARGATE") || get_env_var_or_empty("AWS_EXECUTION_ENV") == "AWS_ECS_FARGATE"
67}
68
69fn file_exists<P>(path: P) -> bool
70where
71 P: AsRef<Path>,
72{
73 std::fs::metadata(path).is_ok()
74}
75
76#[cfg(unix)]
77fn path_empty<P>(path: P) -> bool
78where
79 P: AsRef<Path>,
80{
81 path.as_ref().as_os_str().is_empty()
82}
83
84#[cfg(unix)]
85fn path_contains<P>(path: P, fragment: &str) -> bool
86where
87 P: AsRef<Path>,
88{
89 path.as_ref().to_string_lossy().contains(fragment)
90}
91
92#[cfg(unix)]
100fn check_unix_socket(path: &Path) -> Option<bool> {
101 let metadata = match std::fs::metadata(path) {
103 Ok(metadata) => metadata,
104 Err(e) => {
105 debug!(socket_path = %path.to_string_lossy(), error = %e, "Failed to get metadata for socket path.");
106 return None;
107 }
108 };
109 if !metadata.file_type().is_socket() {
110 return None;
111 }
112
113 let socket = match Socket::new_raw(Domain::UNIX, Type::STREAM, None) {
118 Ok(socket) => socket,
119 Err(e) => {
120 debug!(socket_path = %path.to_string_lossy(), error = %e, "Failed to create socket.");
121 return Some(false);
122 }
123 };
124
125 let socket_addr = match SockAddr::unix(path) {
126 Ok(socket_addr) => socket_addr,
127 Err(e) => {
128 debug!(socket_path = %path.to_string_lossy(), error = %e, "Failed to create socket address.");
129 return Some(false);
130 }
131 };
132
133 Some(
134 match socket.connect_timeout(&socket_addr, SOCKET_CHECK_CONNECT_TIMEOUT) {
135 Ok(_) => {
136 let _ = socket.shutdown(Shutdown::Both);
138
139 true
140 }
141 Err(e) => e.kind() != ErrorKind::PermissionDenied,
142 },
143 )
144}
145
146#[cfg(unix)]
147fn find_first_available_unix_socket<I, P>(socket_paths: I) -> Option<PathBuf>
148where
149 I: IntoIterator<Item = P>,
150 P: AsRef<Path>,
151{
152 for socket_path in socket_paths {
153 let socket_path = socket_path.as_ref();
154 let socket_path_str = socket_path.to_string_lossy();
155
156 match check_unix_socket(socket_path) {
157 None => {
158 debug!(socket_path = %socket_path_str, "No file at socket path or not a socket.");
159 continue;
160 }
161 Some(false) => {
162 debug!(socket_path = %socket_path_str, "Found file at socket path but unreachable. (permissions?)");
163 continue;
164 }
165 Some(true) => {
166 debug!(socket_path = %socket_path_str, "Found reachable socket.");
167 return Some(socket_path.to_path_buf());
168 }
169 }
170 }
171
172 None
173}
174
175fn is_running_inside_container() -> bool {
176 let is_containerized = is_env_var_present("DOCKER_DD_AGENT");
178 if is_containerized {
179 debug!("Found non-empty DOCKER_DD_AGENT environment variable. Likely running in a container.");
180 } else {
181 debug!("Did not find DOCKER_DD_AGENT environment variable. Likely not running in a container.");
182 }
183 is_containerized
184}
185
186#[cfg(unix)]
187fn is_running_inside_docker() -> bool {
188 file_exists("/.dockerenv")
192}
193
194#[cfg(unix)]
195fn with_host_mount_prefixes<I, P>(paths: I) -> Vec<PathBuf>
196where
197 I: IntoIterator<Item = P>,
198 P: AsRef<str>,
199{
200 let mut prefixes = vec![];
201
202 let root = PathBuf::from("/");
208 let host_root = PathBuf::from(CONTAINER_HOST_MOUNT_PATH);
209 let is_containerized = is_running_inside_container();
210
211 for path in paths {
212 let path = path.as_ref().trim_start_matches('/');
213
214 prefixes.push(root.join(path));
215
216 if is_containerized {
217 prefixes.push(host_root.join(path));
218 }
219 }
220
221 prefixes
222}
223
224fn has_host_mapped_procfs() -> bool {
225 let path = PathBuf::from(CONTAINER_HOST_MOUNT_PATH).join("proc");
226 is_running_inside_container() && file_exists(&path)
227}
228
229fn has_host_mapped_cgroupfs() -> bool {
230 let path = PathBuf::from(CONTAINER_HOST_MOUNT_PATH).join("sys/fs/cgroup");
231 is_running_inside_container() && file_exists(&path)
232}
233
234fn has_legacy_cgroupfs_root() -> bool {
235 if is_running_inside_container() {
240 return false;
241 }
242
243 match std::fs::metadata(LEGACY_CGROUPFS_ROOT_MARKER) {
247 Ok(_) => true,
248 Err(e) => e.kind() != std::io::ErrorKind::NotFound,
249 }
250}