1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
//! The schema plane (RFC 08 §7): what a producer serves for a type name,
//! and the drift between what was served and what was declared.
use super::asked::Asked;
use serde::Serialize;
/// One type's schema entry as one producer serves it (issue #51).
#[derive(Debug, Clone, Serialize)]
pub struct SchemaRow {
pub producer: String,
pub type_name: String,
pub kind: String,
pub hash: String,
/// The schema document, when the caller asked for the full form.
#[serde(skip_serializing_if = "Option::is_none")]
pub document: Option<serde_json::Value>,
}
/// One producer's served `describe` reply, rendered (issue #51).
///
/// `served = false` is the honest degradation RFC 08 §7 leaves room for —
/// `describe` is a SHOULD, so a producer that serves none has said nothing
/// about its types, which is not the same as having no types.
#[derive(Debug, Clone, Serialize)]
pub struct SchemaDump {
pub producer: String,
pub served: bool,
/// The declaring app, as the served set names it.
#[serde(skip_serializing_if = "Option::is_none")]
pub app: Option<String>,
pub types: Vec<SchemaRow>,
/// Registry-declared type names this producer's set does **not** cover —
/// RFC 08 §7's totality clause, checked where the user is already looking.
/// `NotAsked` = no registry was loaded, so totality was never checked —
/// not asked is not answered no (RFC 09 §5.1 O4); `Asked(vec![])` is the
/// actual clean bill.
#[serde(skip_serializing_if = "Asked::is_not_asked", default)]
pub missing: Asked<Vec<String>>,
}
/// One producer's identity claim for a type name, attributed to the host that
/// made it (#398).
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct SchemaServer {
pub producer: String,
/// The origin that served this claim — the `h-…` host id, or a verbatim
/// service origin (#398).
///
/// `describe` fans in across every host running the producer, so the
/// producer alone does not name a claimant. Without this a mid-rollout
/// fleet reported that a type had two identities and gave no host to go
/// and look at — the finding you can do least with. `"?"` when the reply
/// key did not parse under the base, the same lossy-but-stated convention
/// [`FleetAnswer::origin`](crate::FleetAnswer::origin) uses.
pub origin: String,
/// The `sha256:` identity this producer served, if it served one.
///
/// `NotAsked` means the describe reply carried **no** hash — which is not
/// an empty hash, and is the distinction the flat `(String, String)` shape
/// could not make: two producers that each said nothing compared equal and
/// were reported as agreeing (#370).
#[serde(skip_serializing_if = "Asked::is_not_asked")]
pub hash: Asked<String>,
}
/// What comparing a type name's identity claims established.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DriftVerdict {
/// Two or more producers served *different* identities. A defect —
/// RFC 08 §7 calls it a `doctor` finding in as many words.
Disagree,
/// At least one producer served no identity at all, so agreement cannot
/// be established. **Not a defect**: an unanswered question, and reporting
/// it as agreement was the O4 failure (RFC 09 §5.1) this exists to name.
Unjudgeable,
}
/// One type name's identity claims across the fleet — "a `doctor` finding" by
/// RFC 08 §7's own words (issue #41), with the O4 split #370 added.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct SchemaDrift {
pub type_name: String,
/// Every producer observed serving the name, and what it claimed.
pub servers: Vec<SchemaServer>,
pub verdict: DriftVerdict,
}
/// A type the producer's slice references that its served describe set does
/// not cover — a violation of RFC 08 §7's totality clause.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct TotalityGap {
pub producer: String,
pub missing: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
/// The serialized `SchemaDrift` is a wire contract, and until #398 it had
/// no pin at all — the one report shape in this file with none.
///
/// The `origin` added there is the load-bearing half: a script reading a
/// drift finding needs a host to act on, and a field that only *sometimes*
/// appeared would be worse than one that never did.
#[test]
fn schema_drift_json_shape_is_pinned() {
let drift = SchemaDrift {
type_name: "Health".into(),
servers: vec![
SchemaServer {
producer: "sysinfo".into(),
origin: "h-3fa9c2d41b7e".into(),
hash: Asked::Asked("sha256:abc".into()),
},
SchemaServer {
producer: "sysinfo".into(),
origin: "h-8b1e07af22c9".into(),
// Served no identity: absent on the wire, never `null` and
// never `""` — the two spellings #370 pulled apart.
hash: Asked::NotAsked,
},
],
verdict: DriftVerdict::Disagree,
};
assert_eq!(
serde_json::to_value(&drift).expect("serialize"),
serde_json::json!({
"type_name": "Health",
"servers": [
{
"producer": "sysinfo",
"origin": "h-3fa9c2d41b7e",
"hash": "sha256:abc",
},
{
"producer": "sysinfo",
"origin": "h-8b1e07af22c9",
},
],
"verdict": "disagree",
})
);
}
}