Skip to main content

ignition_core/client/
restart.rs

1//! Restart + diagnostics-probe capabilities (02-05, HLTH-09/10/11).
2//!
3//! - [`RESTART_PATH`]: the one big red button — POST with
4//!   `confirm=true`, `--yes`-guarded at the CLI seam (research Pitfall
5//!   10: it takes the whole gateway down). The gateway answers 200
6//!   with the literal body `true` almost immediately; the ~40 s wait
7//!   is POLLER-side (the 02-04 engine owns it).
8//! - [`SCAN_PROJECTS_PATH`]: igw-cli's harmless project-rescan write
9//!   probe — `ign doctor --check-write` fires it (2xx = write
10//!   permission, 403 = read-only token).
11//! - [`SECURITY_PROPERTIES_PATH`] + [`WEBDEV_ROOT`]: doctor inputs —
12//!   the security config singleton (the 403 three-part diagnosis's
13//!   part 2: what the gateway's read/write permissions actually are)
14//!   and the WebDev route-presence probe root.
15//!
16//! Deliberately ABSENT: `restart-tasks/pending`. Research is explicit
17//! it is *required-restart* config (changes needing a restart), NOT
18//! active-restart status — never use it as a restart-progress signal.
19
20use std::collections::BTreeMap;
21
22use serde::{Deserialize, Serialize};
23
24/// POST path of the restart capability (query param `confirm=true`).
25pub(crate) const RESTART_PATH: &str = "/data/api/v1/restart-tasks/restart";
26
27/// POST path of the project-scan write probe (doctor `--check-write`).
28pub(crate) const SCAN_PROJECTS_PATH: &str = "/data/api/v1/scan/projects";
29
30/// GET path of the security-properties config singleton — the doctor's
31/// permissions deep-dive (02-RESEARCH §Doctor inputs 5b; the resource
32/// singleton read, same family 02-03 verified for the connection
33/// lists).
34pub(crate) const SECURITY_PROPERTIES_PATH: &str =
35    "/data/api/v1/resources/ignition/security-properties";
36
37/// Root of the WebDev route surface — doctor probes
38/// `/system/webdev/<route>` for presence (404 = absent).
39pub(crate) const WEBDEV_ROOT: &str = "/system/webdev/";
40
41/// GET `/data/api/v1/resources/ignition/security-properties` — the
42/// gateway security config singleton. `readPermissions` /
43/// `writePermissions` are raw passthrough: their populated value shape
44/// was NOT live-captured (the research rig read them as config trees),
45/// so doctor surfaces them verbatim rather than typing a guess.
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47pub struct SecurityProperties {
48    /// The read-permission wiring (passthrough; e.g. an AnyOf level
49    /// tree), when reported.
50    #[serde(
51        rename = "readPermissions",
52        default,
53        skip_serializing_if = "Option::is_none"
54    )]
55    pub read_permissions: Option<serde_json::Value>,
56    /// The write-permission wiring (passthrough), when reported.
57    #[serde(
58        rename = "writePermissions",
59        default,
60        skip_serializing_if = "Option::is_none"
61    )]
62    pub write_permissions: Option<serde_json::Value>,
63    /// Unknown keys round-trip (passthrough-shaped `--json`).
64    #[serde(flatten)]
65    pub extra: BTreeMap<String, serde_json::Value>,
66}
67
68/// The full probe path for one WebDev route: `/system/webdev/<route>`
69/// (ignition-mcp's verified URL shape; 02-RESEARCH §Sources).
70pub(crate) fn webdev_route_path(route: &str) -> String {
71    format!("{WEBDEV_ROOT}{route}")
72}
73
74#[cfg(test)]
75mod tests {
76    use super::{SecurityProperties, webdev_route_path};
77
78    /// The doctor probe paths, contract-pinned (the plan's key_links:
79    /// `security-properties` + `scan/projects` — every wire path in
80    /// this codebase carries a literal pin).
81    #[test]
82    fn doctor_probe_paths_pinned() {
83        assert_eq!(
84            super::SECURITY_PROPERTIES_PATH,
85            "/data/api/v1/resources/ignition/security-properties"
86        );
87        assert_eq!(super::WEBDEV_ROOT, "/system/webdev/");
88        assert_eq!(super::SCAN_PROJECTS_PATH, "/data/api/v1/scan/projects");
89        assert_eq!(super::RESTART_PATH, "/data/api/v1/restart-tasks/restart");
90        assert_eq!(webdev_route_path("stacked"), "/system/webdev/stacked");
91    }
92
93    /// The singleton parses with both permission blocks surfaced under
94    /// their gateway-native names, unknown keys passthrough.
95    #[test]
96    fn security_properties_parses_and_passes_through() {
97        let props: SecurityProperties = serde_json::from_value(serde_json::json!({
98            "readPermissions": {"anyOf": ["Authenticated/Roles/Administrator"]},
99            "writePermissions": {"anyOf": ["Authenticated/Roles/Administrator"]},
100            "secureChannelRequired": true
101        }))
102        .expect("singleton shape must parse");
103        assert!(props.read_permissions.is_some());
104        assert!(props.write_permissions.is_some());
105        assert!(
106            props.extra.contains_key("secureChannelRequired"),
107            "unknown keys round-trip"
108        );
109
110        // Round-trip keeps the gateway-native key names.
111        let round = serde_json::to_value(&props).expect("serialize");
112        assert!(round.get("readPermissions").is_some());
113        assert!(round.get("writePermissions").is_some());
114    }
115
116    /// A sparse singleton (no permission blocks) parses — they are
117    /// Option on purpose.
118    #[test]
119    fn security_properties_tolerates_sparse_bodies() {
120        let props: SecurityProperties =
121            serde_json::from_value(serde_json::json!({"name": "whk"})).expect("sparse body parses");
122        assert_eq!(props.read_permissions, None);
123        assert_eq!(props.write_permissions, None);
124    }
125}