1use std::fmt::{self, Write as _};
11
12use serde_json::Value;
13
14use crate::observe::HttpMethod;
15use crate::path::JsonPath;
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum Side {
20 Engenho,
21 K3s,
22}
23
24impl Side {
25 #[must_use]
27 pub fn label(self) -> &'static str {
28 match self {
29 Self::Engenho => "engenho",
30 Self::K3s => "k3s",
31 }
32 }
33}
34
35impl fmt::Display for Side {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 write!(f, "{}", self.label())
38 }
39}
40
41#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct Gvr {
46 pub group: String,
47 pub version: String,
48 pub resource: String,
49}
50
51impl fmt::Display for Gvr {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 if self.group.is_empty() {
54 write!(f, "{}/{}", self.version, self.resource)
55 } else {
56 write!(f, "{}/{}/{}", self.group, self.version, self.resource)
57 }
58 }
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63pub enum Verb {
64 Get,
65 List,
66 Create,
67 Update,
68 Patch,
69 Delete,
70 Watch,
71 DeleteCollection,
72}
73
74impl Verb {
75 #[must_use]
77 pub fn label(self) -> &'static str {
78 match self {
79 Self::Get => "get",
80 Self::List => "list",
81 Self::Create => "create",
82 Self::Update => "update",
83 Self::Patch => "patch",
84 Self::Delete => "delete",
85 Self::Watch => "watch",
86 Self::DeleteCollection => "deletecollection",
87 }
88 }
89}
90
91impl fmt::Display for Verb {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 write!(f, "{}", self.label())
94 }
95}
96
97#[derive(Clone, Debug, PartialEq, Eq)]
100pub struct StatusCause {
101 pub field: String,
102 pub reason: String,
103 pub message: String,
104}
105
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
109pub enum Severity {
110 Hard,
112 Cosmetic,
115}
116
117impl Severity {
118 #[must_use]
119 pub fn label(self) -> &'static str {
120 match self {
121 Self::Hard => "HARD",
122 Self::Cosmetic => "cosmetic",
123 }
124 }
125}
126
127#[derive(Clone, Debug, PartialEq)]
130pub enum Divergence {
131 StatusCodeDiff {
133 op: String,
134 method: HttpMethod,
135 engenho: u16,
136 k3s: u16,
137 },
138 MissingResource { path: JsonPath, present_on: Side },
140 FieldDiff {
142 path: JsonPath,
143 engenho: Value,
144 k3s: Value,
145 severity: Severity,
146 },
147 MissingDefault {
149 path: JsonPath,
150 k3s_value: Value,
151 severity: Severity,
152 },
153 ExtraField {
155 path: JsonPath,
156 side: Side,
157 value: Value,
158 severity: Severity,
159 },
160 MissingVerb {
162 gvr: Gvr,
163 verb: Verb,
164 present_on: Side,
165 },
166 MissingSubresource {
168 gvr: Gvr,
169 sub: String,
170 present_on: Side,
171 },
172 ValidationShapeDiff {
174 engenho: StatusCause,
175 k3s: StatusCause,
176 },
177 DiscoveryDiff {
179 group: String,
180 version: String,
181 detail: String,
182 },
183 WatchEventDiff { index: usize, detail: String },
185}
186
187impl Divergence {
188 #[must_use]
191 pub fn severity(&self) -> Severity {
192 match self {
193 Self::FieldDiff { severity, .. }
194 | Self::MissingDefault { severity, .. }
195 | Self::ExtraField { severity, .. } => *severity,
196 _ => Severity::Hard,
197 }
198 }
199
200 #[must_use]
204 pub fn owning_crate(&self) -> &'static str {
205 match self {
206 Self::StatusCodeDiff { .. } | Self::MissingVerb { .. } => "engenho-apiserver",
209 Self::MissingResource { .. } => "engenho-apiserver / engenho-controllers",
213 Self::MissingDefault { .. } => {
217 "engenho-apiserver (handler defaulting) / engenho-controllers"
218 }
219 Self::ExtraField { .. } => "engenho-apiserver (handler stamping)",
221 Self::FieldDiff { .. } => "engenho-apiserver / engenho-store",
224 Self::MissingSubresource { .. } => "engenho-apiserver (router subresources)",
226 Self::ValidationShapeDiff { .. } => "engenho-apiserver (admission/validation)",
228 Self::DiscoveryDiff { .. } => "engenho-apiserver (discovery.rs)",
230 Self::WatchEventDiff { .. } => "engenho-store (watch) / engenho-apiserver",
232 }
233 }
234
235 #[must_use]
241 pub fn reroot(self, focus: &JsonPath) -> Self {
242 match self {
243 Self::MissingResource { path, present_on } => Self::MissingResource {
244 path: focus.concat(&path),
245 present_on,
246 },
247 Self::FieldDiff {
248 path,
249 engenho,
250 k3s,
251 severity,
252 } => Self::FieldDiff {
253 path: focus.concat(&path),
254 engenho,
255 k3s,
256 severity,
257 },
258 Self::MissingDefault {
259 path,
260 k3s_value,
261 severity,
262 } => Self::MissingDefault {
263 path: focus.concat(&path),
264 k3s_value,
265 severity,
266 },
267 Self::ExtraField {
268 path,
269 side,
270 value,
271 severity,
272 } => Self::ExtraField {
273 path: focus.concat(&path),
274 side,
275 value,
276 severity,
277 },
278 other => other,
279 }
280 }
281
282 #[must_use]
288 pub fn signature(&self) -> String {
289 let mut s = String::new();
290 let _ = match self {
292 Self::StatusCodeDiff { op, method, .. } => {
293 write!(s, "StatusCodeDiff:{op}:{}", method.label())
294 }
295 Self::MissingResource { path, present_on } => {
296 write!(s, "MissingResource:{path}:{present_on}")
297 }
298 Self::FieldDiff { path, .. } => write!(s, "FieldDiff:{path}"),
299 Self::MissingDefault { path, .. } => write!(s, "MissingDefault:{path}"),
300 Self::ExtraField { path, side, .. } => write!(s, "ExtraField:{path}:{side}"),
301 Self::MissingVerb {
302 gvr,
303 verb,
304 present_on,
305 } => write!(s, "MissingVerb:{gvr}:{verb}:{present_on}"),
306 Self::MissingSubresource {
307 gvr,
308 sub,
309 present_on,
310 } => write!(s, "MissingSubresource:{gvr}:{sub}:{present_on}"),
311 Self::ValidationShapeDiff { .. } => write!(s, "ValidationShapeDiff"),
312 Self::DiscoveryDiff {
313 group,
314 version,
315 detail,
316 } => write!(s, "DiscoveryDiff:{group}/{version}:{detail}"),
317 Self::WatchEventDiff { index, .. } => write!(s, "WatchEventDiff:{index}"),
318 };
319 s
320 }
321}
322
323impl fmt::Display for Divergence {
324 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325 let sev = self.severity().label();
326 match self {
327 Self::StatusCodeDiff {
328 op,
329 method,
330 engenho,
331 k3s,
332 } => write!(
333 f,
334 "[{sev}] StatusCodeDiff {op} ({}): engenho={engenho} k3s={k3s}",
335 method.label()
336 ),
337 Self::MissingResource { path, present_on } => {
338 write!(
339 f,
340 "[{sev}] MissingResource {path}: present only on {present_on}"
341 )
342 }
343 Self::FieldDiff {
344 path, engenho, k3s, ..
345 } => write!(f, "[{sev}] FieldDiff {path}: engenho={engenho} k3s={k3s}"),
346 Self::MissingDefault {
347 path, k3s_value, ..
348 } => write!(
349 f,
350 "[{sev}] MissingDefault {path}: k3s sets {k3s_value}, engenho unset"
351 ),
352 Self::ExtraField {
353 path, side, value, ..
354 } => write!(f, "[{sev}] ExtraField {path}: {side} adds {value}"),
355 Self::MissingVerb {
356 gvr,
357 verb,
358 present_on,
359 } => write!(
360 f,
361 "[{sev}] MissingVerb {gvr} verb={verb}: present only on {present_on}"
362 ),
363 Self::MissingSubresource {
364 gvr,
365 sub,
366 present_on,
367 } => write!(
368 f,
369 "[{sev}] MissingSubresource {gvr}/{sub}: present only on {present_on}"
370 ),
371 Self::ValidationShapeDiff { engenho, k3s } => write!(
372 f,
373 "[{sev}] ValidationShapeDiff: engenho={engenho:?} k3s={k3s:?}"
374 ),
375 Self::DiscoveryDiff {
376 group,
377 version,
378 detail,
379 } => write!(f, "[{sev}] DiscoveryDiff {group}/{version}: {detail}"),
380 Self::WatchEventDiff { index, detail } => {
381 write!(f, "[{sev}] WatchEventDiff #{index}: {detail}")
382 }
383 }
384 }
385}
386
387#[derive(Clone, Copy, Debug, PartialEq, Eq)]
391pub struct ParityWitness(());
392
393impl ParityWitness {
394 pub(crate) fn new() -> Self {
396 Self(())
397 }
398}
399
400#[derive(Clone, Debug, PartialEq)]
402pub enum Verdict {
403 Parity(ParityWitness),
405 Divergent(Vec<Divergence>),
407 ReferenceUnreachable,
410}
411
412impl Verdict {
413 #[must_use]
415 pub fn is_parity(&self) -> bool {
416 matches!(self, Self::Parity(_))
417 }
418
419 #[must_use]
421 pub fn divergences(&self) -> &[Divergence] {
422 match self {
423 Self::Divergent(d) => d,
424 _ => &[],
425 }
426 }
427}