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";
22#[cfg(unix)]
23const SOCKET_CHECK_CONNECT_TIMEOUT: Duration = Duration::from_millis(500);
24
25#[bitmask(u16)]
30#[bitmask_config(vec_debug)]
31pub enum Feature {
32 HostMappedProcfs,
37
38 HostMappedCgroupfs,
43
44 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
56pub 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#[cfg(unix)]
92fn check_unix_socket(path: &Path) -> Option<bool> {
93 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 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 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 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 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 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}