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}