Skip to main content

trusty_console/detect/
agents.rs

1//! `ServiceConnector` implementation for `trusty-agents` (#3331).
2//!
3//! Why: the loopback-only doctrine (#3328) makes the trusty-console reverse
4//! proxy the intended remote path to the trusty-agents API (which now binds
5//! `127.0.0.1` by default, #3329). For the `/api/agents/*` proxy route to
6//! resolve an upstream URL, the connector must surface the daemon's live base
7//! URL — exactly as the trusty-mpm connector does.
8//! What: `AgentsConnector` implements `ServiceConnector::detect()` using the
9//! standard `trusty-common` `http_addr` discovery file written by the daemon
10//! after bind (`serve_with_config` calls `write_daemon_addr("trusty-agents")`).
11//! Path: binary check (`tagent`) → `resolve_data_dir("trusty-agents")/http_addr`
12//! → TCP probe → `Running`/`Available`/`Absent`. The daemon writes no TOML
13//! lock file, so there is no lock-file fallback (unlike the mpm connector).
14//! Test: `agents_connector_absent_binary`, `agents_connector_no_addr_file`,
15//! `agents_connector_surfaces_url_via_http_addr` below.
16
17use crate::connector::{ServiceConnector, ServiceInfo, ServiceLifecycle, ServiceStatus};
18
19use super::helpers::{binary_on_path, detect_service};
20
21/// ServiceConnector for `trusty-agents`.
22///
23/// Why: surfaces the running trusty-agents API daemon in the console Overview
24/// and enables the `/api/agents/*` reverse-proxy route by providing the
25/// daemon's live base URL via the standard `http_addr` discovery file (#3331).
26/// What: implements `detect()` using the standard `trusty-common` data-dir
27/// discovery path (`resolve_data_dir("trusty-agents")/http_addr`) written by
28/// the daemon on bind. The primary (`tagent`) binary is the presence gate.
29/// Test: unit tests below; run with `cargo test -p trusty-console`.
30pub struct AgentsConnector {
31    _priv: (),
32}
33
34impl AgentsConnector {
35    /// Create a new `AgentsConnector`.
36    ///
37    /// Why: production callers use `new()`; there is no home override because
38    /// detection reads the OS data dir via `resolve_data_dir` (overridable in
39    /// tests through `TRUSTY_DATA_DIR_OVERRIDE`).
40    /// What: stores no state.
41    /// Test: created in `all_connectors()` and in unit tests.
42    pub fn new() -> Self {
43        Self { _priv: () }
44    }
45}
46
47impl Default for AgentsConnector {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl ServiceConnector for AgentsConnector {
54    fn id(&self) -> &'static str {
55        "trusty-agents"
56    }
57
58    fn display_name(&self) -> &'static str {
59        "Trusty Agents"
60    }
61
62    /// Detect trusty-agents status, surfacing the daemon URL when reachable.
63    ///
64    /// Why: the `/api/agents/*` proxy handler resolves the daemon base URL from
65    /// this connector's `ServiceInfo.url`, so `detect()` must surface a URL when
66    /// the daemon is reachable via the standard `http_addr` discovery file.
67    /// What: binary check (`tagent`) →
68    /// `resolve_data_dir("trusty-agents")/http_addr` → TCP probe → `Running`
69    /// with `url: Some(base_url)`; binary present but no reachable addr file →
70    /// `Available`; binary absent → `Absent`. Delegates to the shared
71    /// `detect_service()` helper (addr-file read, TCP probe, version fetch).
72    /// Test: `agents_connector_surfaces_url_via_http_addr` (primary path),
73    /// `agents_connector_no_addr_file`, `agents_connector_absent_binary`.
74    fn detect(&self) -> ServiceInfo {
75        // `resolve_data_dir` is infallible in practice; if the data directory
76        // cannot be resolved, report status purely on binary presence.
77        if let Ok(dir) = trusty_common::resolve_data_dir("trusty-agents") {
78            return detect_service(
79                self.id(),
80                self.display_name(),
81                "tagent",
82                dir.join("http_addr"),
83            );
84        }
85
86        ServiceInfo {
87            id: self.id().to_string(),
88            display_name: self.display_name().to_string(),
89            status: if binary_on_path("tagent") {
90                ServiceStatus::Available
91            } else {
92                ServiceStatus::Absent
93            },
94            version: None,
95            url: None,
96            hint: None,
97            // #6416: trusty-agents is a resident daemon — `Available` here means
98            // installed but stopped, which is what the card should say.
99            lifecycle: ServiceLifecycle::Daemon,
100        }
101    }
102}
103
104// ─── tests ────────────────────────────────────────────────────────────────────
105
106#[cfg(test)]
107mod tests {
108    use super::super::ENV_LOCK;
109    use super::*;
110    use std::fs;
111    use std::net::TcpListener;
112    use tempfile::TempDir;
113    use trusty_common::DATA_DIR_OVERRIDE_ENV;
114
115    /// Why: with no binary on PATH the connector must report Absent regardless
116    /// of any stale discovery file.
117    /// Test: this test.
118    #[test]
119    fn agents_connector_absent_binary() {
120        // Only meaningful when the binary is genuinely not installed (CI).
121        if which::which("tagent").is_ok() {
122            return;
123        }
124        let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
125        let data_tmp = TempDir::new().expect("data-tempdir");
126        unsafe {
127            std::env::set_var(DATA_DIR_OVERRIDE_ENV, data_tmp.path());
128        }
129        let info = AgentsConnector::new().detect();
130        unsafe {
131            std::env::remove_var(DATA_DIR_OVERRIDE_ENV);
132        }
133        assert_eq!(info.status, ServiceStatus::Absent);
134        assert_eq!(info.id, "trusty-agents");
135        assert_eq!(info.display_name, "Trusty Agents");
136    }
137
138    /// Why: no http_addr file with the binary present must yield Available (not
139    /// Running); binary absent yields Absent.
140    /// Test: this test.
141    #[test]
142    fn agents_connector_no_addr_file() {
143        let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
144        let data_tmp = TempDir::new().expect("data-tempdir");
145        unsafe {
146            std::env::set_var(DATA_DIR_OVERRIDE_ENV, data_tmp.path());
147        }
148        let info = AgentsConnector::new().detect();
149        unsafe {
150            std::env::remove_var(DATA_DIR_OVERRIDE_ENV);
151        }
152        assert!(info.url.is_none());
153        if which::which("tagent").is_ok() {
154            assert_eq!(info.status, ServiceStatus::Available);
155        } else {
156            assert_eq!(info.status, ServiceStatus::Absent);
157        }
158    }
159
160    /// Why: the primary path (#3331) must surface `url: Some(base_url)` when the
161    /// http_addr file exists and the port is reachable — this is what the proxy
162    /// handler reads to forward `/api/agents/*` requests.
163    /// What: writes a valid addr to the standard http_addr file under a temp
164    /// TRUSTY_DATA_DIR_OVERRIDE, binds a real listening port so tcp_probe passes,
165    /// calls detect(), and asserts the url and Running status.
166    /// Test: this test (key regression guard for #3331).
167    #[test]
168    fn agents_connector_surfaces_url_via_http_addr() {
169        let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
170        let data_tmp = TempDir::new().expect("data-tempdir");
171        unsafe {
172            std::env::set_var(DATA_DIR_OVERRIDE_ENV, data_tmp.path());
173        }
174        // Write the http_addr file with a listening port so tcp_probe passes.
175        let agents_dir = data_tmp.path().join("trusty-agents");
176        fs::create_dir_all(&agents_dir).expect("mkdir");
177        let listener = TcpListener::bind("127.0.0.1:0").expect("bind free port");
178        let addr = listener.local_addr().expect("local_addr").to_string();
179        fs::write(agents_dir.join("http_addr"), &addr).expect("write addr");
180
181        let info = AgentsConnector::new().detect();
182
183        // Drop listener after detect() so the port is open during the probe.
184        drop(listener);
185        unsafe {
186            std::env::remove_var(DATA_DIR_OVERRIDE_ENV);
187        }
188
189        if which::which("tagent").is_ok() {
190            assert_eq!(
191                info.status,
192                ServiceStatus::Running,
193                "http_addr present + port open must yield Running, got: {info:?}"
194            );
195            assert_eq!(
196                info.url,
197                Some(format!("http://{addr}")),
198                "Running status must include daemon base URL for proxy routing"
199            );
200        } else {
201            assert_eq!(info.status, ServiceStatus::Absent);
202        }
203    }
204}