Skip to main content

container_device_interface/schema/
mod.rs

1use 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}