Skip to main content

execgo_runtime/
capabilities.rs

1use std::{
2    collections::BTreeMap,
3    fs,
4    path::{Path, PathBuf},
5};
6
7use chrono::Utc;
8
9use crate::types::{
10    ExecutionCapabilities, NamespaceCapabilities, ResourceCapabilities, ResourceCapacity,
11    RuntimeCapabilities, RuntimePlatform, SandboxCapabilities, StorageCapabilities,
12};
13
14#[derive(Debug, Clone)]
15pub struct CapabilityProbeInput {
16    pub runtime_id: String,
17    pub data_dir: PathBuf,
18    pub cgroup_root: PathBuf,
19    pub max_running_tasks: usize,
20    pub disable_linux_sandbox: bool,
21    pub disable_cgroup: bool,
22    pub capacity_memory_bytes: Option<u64>,
23    pub capacity_pids: Option<u64>,
24}
25
26pub fn probe_runtime_capabilities(input: &CapabilityProbeInput) -> RuntimeCapabilities {
27    let mut warnings = Vec::new();
28    let mut overrides = BTreeMap::new();
29
30    if input.disable_linux_sandbox {
31        overrides.insert("linux_sandbox".into(), "disabled".into());
32    }
33    if input.disable_cgroup {
34        overrides.insert("cgroup".into(), "disabled".into());
35    }
36    if let Some(value) = input.capacity_memory_bytes {
37        overrides.insert("capacity_memory_bytes".into(), value.to_string());
38    }
39    if let Some(value) = input.capacity_pids {
40        overrides.insert("capacity_pids".into(), value.to_string());
41    }
42
43    let data_dir_writable = data_dir_writable(&input.data_dir);
44    if !data_dir_writable {
45        warnings.push(format!(
46            "data-dir is not writable: {}",
47            input.data_dir.to_string_lossy()
48        ));
49    }
50
51    let platform = RuntimePlatform {
52        os: std::env::consts::OS.to_string(),
53        arch: std::env::consts::ARCH.to_string(),
54        containerized: detect_containerized(),
55        kubernetes: detect_kubernetes(),
56    };
57
58    let linux = cfg!(target_os = "linux");
59    let root_user = current_euid() == Some(0);
60    let linux_sandbox = linux && root_user && !input.disable_linux_sandbox;
61    if !linux_sandbox {
62        warnings.push(if linux {
63            "linux_sandbox is unavailable without root-equivalent namespace permissions".into()
64        } else {
65            "linux_sandbox is unavailable on this host".into()
66        });
67    }
68
69    let cgroup_v2 = linux && Path::new("/sys/fs/cgroup/cgroup.controllers").exists();
70    let cgroup_writable =
71        cgroup_v2 && !input.disable_cgroup && path_likely_writable(&input.cgroup_root);
72    if cgroup_v2 && !cgroup_writable {
73        warnings.push(format!(
74            "cgroup v2 detected but cgroup root is not writable: {}",
75            input.cgroup_root.to_string_lossy()
76        ));
77    }
78
79    let memory_capacity = input.capacity_memory_bytes.or_else(detect_memory_bytes);
80    let pids_capacity = input.capacity_pids.or_else(detect_pids_capacity);
81
82    let resources = ResourceCapabilities {
83        rlimit_cpu: cfg!(unix),
84        rlimit_memory: cfg!(unix),
85        cgroup_v2,
86        cgroup_writable,
87        memory_limit: cfg!(unix),
88        pids_limit: cgroup_writable,
89        oom_detection: cgroup_writable,
90        cpu_quota: false,
91        ledger: true,
92        capacity: ResourceCapacity {
93            task_slots: input.max_running_tasks as u64,
94            memory_bytes: memory_capacity,
95            pids: pids_capacity,
96        },
97    };
98
99    RuntimeCapabilities {
100        runtime_id: input.runtime_id.clone(),
101        snapshot_version: RuntimeCapabilities::snapshot_version().to_string(),
102        collected_at: Utc::now(),
103        platform,
104        execution: ExecutionCapabilities {
105            command: true,
106            script: true,
107            process_group: cfg!(unix),
108        },
109        sandbox: SandboxCapabilities {
110            process: true,
111            linux_sandbox,
112            chroot: linux_sandbox,
113            namespaces: NamespaceCapabilities {
114                mount: linux_sandbox,
115                pid: linux_sandbox,
116                uts: linux_sandbox,
117                ipc: linux_sandbox,
118                net: linux_sandbox,
119            },
120        },
121        storage: StorageCapabilities { data_dir_writable },
122        resources,
123        stable_semantics: stable_semantics(),
124        enhanced_semantics: enhanced_semantics(linux_sandbox, cgroup_writable),
125        degraded: !warnings.is_empty(),
126        warnings,
127        overrides,
128    }
129}
130
131fn stable_semantics() -> Vec<String> {
132    [
133        "submit",
134        "status",
135        "events",
136        "stdout_stderr",
137        "timeout",
138        "kill",
139        "artifacts",
140        "result_persistence",
141        "recovery",
142    ]
143    .into_iter()
144    .map(str::to_string)
145    .collect()
146}
147
148fn enhanced_semantics(linux_sandbox: bool, cgroup_writable: bool) -> Vec<String> {
149    let mut items = vec!["resource_ledger".to_string()];
150    if linux_sandbox {
151        items.extend([
152            "linux_sandbox".to_string(),
153            "namespaces".to_string(),
154            "chroot".to_string(),
155        ]);
156    }
157    if cgroup_writable {
158        items.extend([
159            "cgroup_memory".to_string(),
160            "cgroup_pids".to_string(),
161            "oom_detection".to_string(),
162        ]);
163    }
164    items
165}
166
167fn data_dir_writable(path: &Path) -> bool {
168    let probe_path = path.join(format!(".execgo-runtime-probe-{}", std::process::id()));
169    match fs::write(&probe_path, b"probe") {
170        Ok(()) => {
171            let _ = fs::remove_file(probe_path);
172            true
173        }
174        Err(_) => false,
175    }
176}
177
178fn path_likely_writable(path: &Path) -> bool {
179    let candidate = if path.exists() {
180        path
181    } else {
182        path.parent().unwrap_or(path)
183    };
184    fs::metadata(candidate)
185        .map(|metadata| !metadata.permissions().readonly())
186        .unwrap_or(false)
187}
188
189fn detect_containerized() -> bool {
190    Path::new("/.dockerenv").exists()
191        || Path::new("/run/.containerenv").exists()
192        || read_to_string("/proc/1/cgroup").is_some_and(|contents| {
193            contents.contains("docker")
194                || contents.contains("containerd")
195                || contents.contains("kubepods")
196        })
197}
198
199fn detect_kubernetes() -> bool {
200    std::env::var_os("KUBERNETES_SERVICE_HOST").is_some()
201        || read_to_string("/proc/1/cgroup").is_some_and(|contents| contents.contains("kubepods"))
202}
203
204fn detect_memory_bytes() -> Option<u64> {
205    #[cfg(unix)]
206    unsafe {
207        let pages = libc::sysconf(libc::_SC_PHYS_PAGES);
208        let page_size = libc::sysconf(libc::_SC_PAGESIZE);
209        if pages > 0 && page_size > 0 {
210            return Some((pages as u64).saturating_mul(page_size as u64));
211        }
212    }
213    None
214}
215
216fn detect_pids_capacity() -> Option<u64> {
217    #[cfg(target_os = "linux")]
218    {
219        read_to_string("/proc/sys/kernel/pid_max")
220            .and_then(|value| value.trim().parse::<u64>().ok())
221    }
222    #[cfg(not(target_os = "linux"))]
223    {
224        None
225    }
226}
227
228fn current_euid() -> Option<u32> {
229    #[cfg(unix)]
230    unsafe {
231        Some(libc::geteuid())
232    }
233    #[cfg(not(unix))]
234    {
235        None
236    }
237}
238
239fn read_to_string(path: &str) -> Option<String> {
240    fs::read_to_string(path).ok()
241}
242
243#[cfg(test)]
244mod tests {
245    use tempfile::TempDir;
246
247    use super::*;
248
249    #[test]
250    fn probe_returns_stable_shape() {
251        let temp = TempDir::new().expect("tempdir");
252        let capabilities = probe_runtime_capabilities(&CapabilityProbeInput {
253            runtime_id: "test-runtime".into(),
254            data_dir: temp.path().to_path_buf(),
255            cgroup_root: temp.path().join("cgroup"),
256            max_running_tasks: 3,
257            disable_linux_sandbox: true,
258            disable_cgroup: true,
259            capacity_memory_bytes: Some(1024),
260            capacity_pids: Some(64),
261        });
262
263        assert_eq!(capabilities.runtime_id, "test-runtime");
264        assert_eq!(capabilities.resources.capacity.task_slots, 3);
265        assert_eq!(capabilities.resources.capacity.memory_bytes, Some(1024));
266        assert!(!capabilities.sandbox.linux_sandbox);
267        assert_eq!(
268            capabilities
269                .overrides
270                .get("linux_sandbox")
271                .map(String::as_str),
272            Some("disabled")
273        );
274    }
275}