Skip to main content

resource_tracker/collector/clouds/
mod.rs

1use crate::metrics::CloudInfo;
2use std::time::Duration;
3use ureq::config::Config as UreqConfig;
4
5mod alicloud;
6mod aws;
7mod azure;
8mod gcp;
9mod hetzner;
10mod ovh;
11mod upcloud;
12
13// ---------------------------------------------------------------------------
14// Shared IMDS helpers (available to all cloud submodules via `super::`)
15// ---------------------------------------------------------------------------
16
17/// Upper bound for each HTTP call made by a vendor probe.
18const IMDS_TIMEOUT: Duration = Duration::from_secs(1);
19
20fn new_imds_agent() -> ureq::Agent {
21    UreqConfig::builder()
22        .timeout_global(Some(IMDS_TIMEOUT))
23        .build()
24        .new_agent()
25}
26
27fn imds_get(agent: &ureq::Agent, url: &str) -> Option<String> {
28    imds_get_headers(agent, url, &[])
29}
30
31fn imds_get_headers(agent: &ureq::Agent, url: &str, headers: &[(&str, &str)]) -> Option<String> {
32    let mut req = agent.get(url);
33    for (k, v) in headers {
34        req = req.header(*k, *v);
35    }
36    req.call()
37        .ok()
38        .and_then(|mut r| r.body_mut().read_to_string().ok())
39        .map(|s| s.trim().to_string())
40        .filter(|s| !s.is_empty())
41}
42
43// ---------------------------------------------------------------------------
44// Probe orchestration
45// ---------------------------------------------------------------------------
46
47/// Precedence order: AWS → GCP → Azure → Hetzner → UpCloud → AliCloud → OVH.
48/// To add a new cloud: implement `pub fn probe() -> Option<CloudInfo>` in a new
49/// submodule, declare it above, and append it here.
50const PROBES: &[fn() -> Option<CloudInfo>] = &[
51    aws::probe,
52    gcp::probe,
53    azure::probe,
54    hetzner::probe,
55    upcloud::probe,
56    alicloud::probe,
57    ovh::probe,
58];
59
60/// Run all vendor probes in parallel (one OS thread per vendor).
61///
62/// Join order follows the `PROBES` precedence list: the first successful probe
63/// wins and the remaining [`JoinHandle`]s are dropped without joining, so those
64/// threads keep running until their own timeouts (avoids waiting for every
65/// vendor on, e.g., a confirmed AWS host). Each HTTP call uses [`IMDS_TIMEOUT`].
66fn probe_cloud() -> CloudInfo {
67    let handles: Vec<_> = PROBES.iter().map(|&p| std::thread::spawn(p)).collect();
68    for handle in handles {
69        if let Ok(Some(info)) = handle.join() {
70            return info;
71        }
72    }
73    CloudInfo::default()
74}
75
76/// Spawn a background thread that probes cloud IMDS endpoints.
77///
78/// Call this **before** the warm-up sleep so probes run **in parallel** with the
79/// main thread's warm-up (stateful collector priming + one `interval` sleep).
80/// Join the handle **after** warm-up to read results; if probes finished during
81/// sleep, `join` returns immediately.
82pub fn spawn_cloud_discovery() -> std::thread::JoinHandle<CloudInfo> {
83    std::thread::spawn(probe_cloud)
84}
85
86// ---------------------------------------------------------------------------
87// Tests
88// ---------------------------------------------------------------------------
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    // T-CLOUD-01: spawn_cloud_discovery resolves without panic.
95    // Each vendor's HTTP calls use IMDS_TIMEOUT; all vendor probes run in parallel.
96    #[test]
97    fn test_spawn_cloud_discovery_joins_without_panic() {
98        let handle = spawn_cloud_discovery();
99        let _cloud = handle.join().expect("cloud discovery thread panicked");
100        // Result may be default (no cloud) or populated (running on a cloud VM).
101        // Either outcome is valid; the test only checks for no panic.
102    }
103}