1use std::collections::{BTreeMap, BTreeSet};
4
5use serde::Serialize;
6
7use super::redact::{RedactedCommand, RedactedHeader, RedactedUrl};
8use super::{BuildkitDiagnosticDetails, ContainerdDiagnosticDetails, DockerDiagnosticDetails};
9
10pub const DIAGNOSTIC_SCHEMA_VERSION: u32 = 2;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
14#[serde(rename_all = "kebab-case")]
15pub enum RuntimeKind {
16 Docker,
17 Containerd,
18 Buildkit,
19 Podman,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
24#[serde(rename_all = "kebab-case")]
25pub enum DiagnosticStatus {
26 Healthy,
27 Degraded,
28 NotInstalled,
29 ClientOnly,
30 Unreachable,
31 PermissionDenied,
32 UnsupportedVersion,
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
37#[serde(rename_all = "kebab-case")]
38pub enum Capability {
39 Client,
40 Daemon,
41 RuntimeInfo,
42 Pull,
43 CacheStatus,
44 CachePrune,
45 RegistryMirrors,
46 RegistryHostMapping,
47 BuilderInspection,
48 PlatformSelection,
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
53#[serde(rename_all = "kebab-case")]
54pub enum CapabilityStatus {
55 Supported,
56 Unsupported,
57 Unavailable,
58 Unknown,
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
63#[serde(rename_all = "kebab-case")]
64pub enum Privilege {
65 None,
66 CurrentUser,
67 Root,
68 Administrator,
69 RemoteAdministrator,
70 Unknown,
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
75#[serde(rename_all = "kebab-case")]
76pub enum EndpointTransport {
77 LocalSocket,
78 NamedPipe,
79 Tcp,
80 Http,
81 Https,
82 Ssh,
83 DockerDesktop,
84 Cloud,
85 Unknown,
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
90#[serde(rename_all = "kebab-case")]
91pub enum EndpointScope {
92 Local,
93 Remote,
94 ManagedDesktop,
95 ManagedCloud,
96 Unknown,
97}
98
99#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
101pub struct Endpoint {
102 pub transport: EndpointTransport,
103 pub scope: EndpointScope,
104 pub address: RedactedUrl,
105}
106
107impl Endpoint {
108 pub const fn new(
109 transport: EndpointTransport,
110 scope: EndpointScope,
111 address: RedactedUrl,
112 ) -> Self {
113 Self {
114 transport,
115 scope,
116 address,
117 }
118 }
119}
120
121#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
123#[serde(tag = "kind", content = "value", rename_all = "kebab-case")]
124pub enum DiagnosticEvidence {
125 Endpoint(Endpoint),
126 Header(RedactedHeader),
127 Command(RedactedCommand),
128}
129
130#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
134#[serde(tag = "kind", rename_all = "kebab-case")]
135pub enum DiagnosticDetails {
136 Docker(DockerDiagnosticDetails),
137 Containerd(ContainerdDiagnosticDetails),
138 Buildkit(BuildkitDiagnosticDetails),
139}
140
141#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
145pub struct DiagnosticReport {
146 pub schema_version: u32,
147 pub runtime: RuntimeKind,
148 pub status: DiagnosticStatus,
149 pub details: Option<DiagnosticDetails>,
150 pub privilege: Privilege,
151 pub capabilities: BTreeMap<Capability, CapabilityStatus>,
152 pub endpoints: BTreeSet<Endpoint>,
153 pub evidence: BTreeSet<DiagnosticEvidence>,
154}
155
156impl DiagnosticReport {
157 pub fn new(runtime: RuntimeKind, status: DiagnosticStatus) -> Self {
158 Self {
159 schema_version: DIAGNOSTIC_SCHEMA_VERSION,
160 runtime,
161 status,
162 details: match runtime {
163 RuntimeKind::Docker => {
164 Some(DiagnosticDetails::Docker(DockerDiagnosticDetails::default()))
165 }
166 RuntimeKind::Containerd => Some(DiagnosticDetails::Containerd(
167 ContainerdDiagnosticDetails::default(),
168 )),
169 RuntimeKind::Buildkit => Some(DiagnosticDetails::Buildkit(
170 BuildkitDiagnosticDetails::default(),
171 )),
172 RuntimeKind::Podman => None,
173 },
174 privilege: Privilege::Unknown,
175 capabilities: BTreeMap::new(),
176 endpoints: BTreeSet::new(),
177 evidence: BTreeSet::new(),
178 }
179 }
180
181 pub fn with_privilege(mut self, privilege: Privilege) -> Self {
182 self.privilege = privilege;
183 self
184 }
185
186 pub fn set_details(&mut self, details: DiagnosticDetails) {
187 self.details = Some(details);
188 }
189
190 pub fn set_capability(&mut self, capability: Capability, status: CapabilityStatus) {
191 self.capabilities.insert(capability, status);
192 }
193
194 pub fn add_endpoint(&mut self, endpoint: Endpoint) {
195 self.endpoints.insert(endpoint);
196 }
197
198 pub fn add_evidence(&mut self, evidence: DiagnosticEvidence) {
199 self.evidence.insert(evidence);
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use crate::process::CommandSpec;
206
207 use super::*;
208 use crate::container::redact::{CommandPurpose, NativeProgram};
209
210 fn report(reverse: bool) -> DiagnosticReport {
211 let endpoint_a = Endpoint::new(
212 EndpointTransport::Https,
213 EndpointScope::Remote,
214 RedactedUrl::parse("https://alice:password@z.example/private?token=secret").unwrap(),
215 );
216 let endpoint_b = Endpoint::new(
217 EndpointTransport::LocalSocket,
218 EndpointScope::Local,
219 RedactedUrl::parse("unix:///var/run/docker.sock").unwrap(),
220 );
221 let command = CommandSpec::new("docker").args(["info", "--format", "secret"]);
222 let command = DiagnosticEvidence::Command(RedactedCommand::from_spec(
223 NativeProgram::Docker,
224 CommandPurpose::RuntimeInfo,
225 &command,
226 ));
227
228 let mut report = DiagnosticReport::new(RuntimeKind::Docker, DiagnosticStatus::Healthy)
229 .with_privilege(Privilege::CurrentUser);
230 let capabilities = [
231 (Capability::Pull, CapabilityStatus::Supported),
232 (Capability::Daemon, CapabilityStatus::Supported),
233 ];
234 let endpoints = [endpoint_a, endpoint_b];
235 if reverse {
236 for (capability, status) in capabilities.into_iter().rev() {
237 report.set_capability(capability, status);
238 }
239 for endpoint in endpoints.into_iter().rev() {
240 report.add_endpoint(endpoint);
241 }
242 } else {
243 for (capability, status) in capabilities {
244 report.set_capability(capability, status);
245 }
246 for endpoint in endpoints {
247 report.add_endpoint(endpoint);
248 }
249 }
250 report.add_evidence(command);
251 report
252 }
253
254 #[test]
255 fn diagnostic_serialization_is_schema_versioned_and_deterministic() {
256 let left = serde_json::to_string(&report(false)).unwrap();
257 let right = serde_json::to_string(&report(true)).unwrap();
258
259 assert_eq!(left, right);
260 assert!(left.starts_with("{\"schema_version\":2,"));
261 assert!(left.contains("\"details\":{\"kind\":\"docker\""));
262 assert!(left.contains("\"daemon\":\"supported\""));
263 assert!(left.contains("\"pull\":\"supported\""));
264 for secret in ["alice", "password", "private", "token", "secret"] {
265 assert!(!left.contains(secret), "leaked {secret}: {left}");
266 }
267 }
268}