container_device_interface/schema/
mod.rs1use std::collections::BTreeMap;
2
3use anyhow::{anyhow, Context, Result};
4use jsonschema::{Draft, Validator};
5use once_cell::sync::Lazy;
6use serde_json::Value;
7
8use crate::{
9 internal::validation::validate::validate_spec_annotations, specs::config::Spec as CDISpec,
10 version::validate_declared_version_fields,
11};
12
13const SCHEMA_JSON: &str = include_str!("schema.json");
14const DEFS_JSON: &str = include_str!("defs.json");
15static BUILTIN_SCHEMA: Lazy<Result<Validator, String>> =
16 Lazy::new(|| compile_builtin_schema().map_err(|err| format!("{err:#}")));
17
18pub fn builtin_schema_value() -> Result<Value> {
19 cdi_schema_value(SCHEMA_JSON.as_bytes(), DEFS_JSON.as_bytes())
20}
21
22pub fn cdi_schema_value(schema_data: &[u8], defs_data: &[u8]) -> Result<Value> {
23 let mut schema_json: Value =
24 serde_json::from_slice(schema_data).context("parse CDI schema.json")?;
25 let defs_json: Value = serde_json::from_slice(defs_data).context("parse CDI defs.json")?;
26 rewrite_defs_json_refs(&mut schema_json);
27
28 let schema = schema_json
29 .as_object_mut()
30 .ok_or_else(|| anyhow!("CDI schema must be a JSON object"))?;
31 let definitions = defs_json
32 .get("definitions")
33 .cloned()
34 .ok_or_else(|| anyhow!("CDI defs.json must contain definitions"))?;
35 schema.insert("definitions".to_string(), definitions);
36
37 Ok(schema_json)
38}
39
40fn rewrite_defs_json_refs(value: &mut Value) {
41 match value {
42 Value::Object(object) => {
43 if let Some(Value::String(reference)) = object.get_mut("$ref") {
44 if let Some(definition) = reference.strip_prefix("defs.json#/definitions/") {
45 *reference = format!("#/definitions/{definition}");
46 }
47 }
48
49 for value in object.values_mut() {
50 rewrite_defs_json_refs(value);
51 }
52 }
53 Value::Array(values) => {
54 for value in values {
55 rewrite_defs_json_refs(value);
56 }
57 }
58 _ => {}
59 }
60}
61
62pub fn compile_builtin_schema() -> Result<Validator> {
63 let schema_json = builtin_schema_value()?;
64 Validator::options()
65 .with_draft(Draft::Draft7)
66 .build(&schema_json)
67 .context("compile builtin CDI schema")
68}
69
70pub fn compile_cdi_schema(schema_data: &[u8], defs_data: &[u8]) -> Result<Validator> {
71 let schema_json = cdi_schema_value(schema_data, defs_data)?;
72 Validator::options()
73 .with_draft(Draft::Draft7)
74 .build(&schema_json)
75 .context("compile CDI schema")
76}
77
78pub fn document_value(doc_data: &[u8]) -> Result<Value> {
79 let yaml_value: serde_yaml::Value =
80 serde_yaml::from_slice(doc_data).context("parse CDI document")?;
81 serde_json::to_value(yaml_value).context("convert CDI document to JSON value")
82}
83
84pub fn validate(schema: &Validator, doc_data: &[u8]) -> Result<()> {
85 let doc = document_value(doc_data)?;
86 validate_value(schema, &doc)
87}
88
89pub fn validate_cdi(schema: &Validator, doc_data: &[u8]) -> Result<()> {
90 let doc = document_value(doc_data)?;
91 validate_value(schema, &doc)?;
92 validate_cdi_document_content(&doc)?;
93 validate_typed_cdi_document(doc_data)
94}
95
96pub fn validate_builtin(doc_data: &[u8]) -> Result<()> {
97 let schema = BUILTIN_SCHEMA
98 .as_ref()
99 .map_err(|err| anyhow!("compile builtin CDI schema: {err}"))?;
100 validate_cdi(schema, doc_data)
101}
102
103fn validate_value(schema: &Validator, doc: &Value) -> Result<()> {
104 let errors: Vec<String> = schema
105 .iter_errors(doc)
106 .map(|error| error.to_string())
107 .collect();
108
109 if errors.is_empty() {
110 return Ok(());
111 }
112
113 Err(anyhow!("schema validation failed: {}", errors.join("; ")))
114}
115
116fn validate_typed_cdi_document(doc_data: &[u8]) -> Result<()> {
117 let spec: CDISpec =
118 serde_yaml::from_slice(doc_data).context("parse CDI document using declared version")?;
119 validate_declared_version_fields(&spec)
120}
121
122fn validate_cdi_document_content(doc: &Value) -> Result<()> {
123 if doc
124 .get("devices")
125 .and_then(Value::as_array)
126 .is_some_and(Vec::is_empty)
127 {
128 return Err(anyhow!(
129 "CDI schema validation failed: top-level devices array must not be empty"
130 ));
131 }
132
133 validate_annotations("", doc.get("annotations"))?;
134
135 if let Some(devices) = doc.get("devices").and_then(Value::as_array) {
136 for device in devices {
137 let name = device
138 .get("name")
139 .and_then(Value::as_str)
140 .unwrap_or_default();
141 validate_annotations(name, device.get("annotations"))?;
142 }
143 }
144
145 Ok(())
146}
147
148fn validate_annotations(name: &str, annotations: Option<&Value>) -> Result<()> {
149 let Some(Value::Object(annotations)) = annotations else {
150 return Ok(());
151 };
152
153 let mut parsed = BTreeMap::new();
154 for (key, value) in annotations {
155 let Some(value) = value.as_str() else {
156 return Err(anyhow!(
157 "invalid annotation {}.{}; annotation value is not a string",
158 name,
159 key
160 ));
161 };
162 parsed.insert(key.clone(), value.to_string());
163 }
164
165 validate_spec_annotations(name, &parsed)
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn builtin_schema_accepts_v1_1_features() {
174 let doc = br#"
175cdiVersion: "1.1.0"
176kind: "vendor.com/device"
177containerEdits:
178 netDevices:
179 - hostInterfaceName: "eth0"
180 name: "container_eth0"
181 intelRdt:
182 schemata:
183 - "L3:0=ffff"
184 enableMonitoring: true
185devices:
186 - name: "gpu0"
187 containerEdits:
188 deviceNodes:
189 - path: "/dev/null"
190"#;
191
192 validate_builtin(doc).expect("v1.1.0 document should validate");
193 }
194
195 #[test]
196 fn builtin_schema_rejects_wrong_type() {
197 let doc = br#"
198cdiVersion: "1.1.0"
199kind: "vendor.com/device"
200devices: "not-an-array"
201"#;
202
203 assert!(validate_builtin(doc).is_err());
204 }
205
206 #[test]
207 fn builtin_schema_rejects_v1_1_legacy_intel_rdt_fields() {
208 let doc = br#"
209cdiVersion: "1.1.0"
210kind: "vendor.com/device"
211containerEdits:
212 intelRdt:
213 enableCMT: true
214devices:
215 - name: "gpu0"
216 containerEdits:
217 deviceNodes:
218 - path: "/dev/null"
219"#;
220
221 let err = validate_builtin(doc).expect_err("v1.1.0 must reject legacy Intel RDT fields");
222
223 assert!(err.to_string().contains("enableCMT"));
224 }
225
226 #[test]
227 fn builtin_schema_rejects_v1_0_intel_rdt_enable_monitoring_field() {
228 let doc = br#"
229cdiVersion: "1.0.0"
230kind: "vendor.com/device"
231containerEdits:
232 intelRdt:
233 enableMonitoring: false
234devices:
235 - name: "gpu0"
236 containerEdits:
237 deviceNodes:
238 - path: "/dev/null"
239"#;
240
241 let err = validate_builtin(doc).expect_err("v1.0.0 must reject v1.1.0 Intel RDT fields");
242
243 assert!(err.to_string().contains("enableMonitoring"));
244 }
245
246 #[test]
247 fn generic_validate_allows_empty_devices_when_schema_allows_it() {
248 let schema_json: Value = serde_json::json!({
249 "type": "object"
250 });
251 let schema = Validator::options()
252 .with_draft(Draft::Draft7)
253 .build(&schema_json)
254 .expect("compile permissive schema");
255 let doc = br#"
256devices: []
257"#;
258
259 validate(&schema, doc).expect("generic validation should only apply the supplied schema");
260 }
261
262 #[test]
263 fn cdi_schema_rejects_invalid_spec_annotations() {
264 let doc = br#"
265cdiVersion: "1.1.0"
266kind: "vendor.com/device"
267annotations:
268 "inva$$lid_CDIKEY": "value"
269devices:
270 - name: "gpu0"
271 containerEdits:
272 deviceNodes:
273 - path: "/dev/null"
274"#;
275
276 let err = validate_builtin(doc).expect_err("invalid annotation key should fail");
277
278 assert!(err.to_string().contains("annotations"));
279 }
280
281 #[test]
282 fn cdi_schema_rejects_invalid_device_annotations() {
283 let doc = br#"
284cdiVersion: "1.1.0"
285kind: "vendor.com/device"
286devices:
287 - name: "gpu0"
288 annotations:
289 "inva$$lid_CDIKEY": "value"
290 containerEdits:
291 deviceNodes:
292 - path: "/dev/null"
293"#;
294
295 let err = validate_builtin(doc).expect_err("invalid device annotation key should fail");
296
297 assert!(err.to_string().contains("gpu0.annotations"));
298 }
299}