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}