Skip to main content

microsandbox_types/cloud/
compat.rs

1//! Backward-compatible deserialization for cloud sandbox creation.
2//!
3//! Serialization always emits the canonical source-tagged union. This module
4//! additionally accepts the legacy untagged `image` and `disk_snapshot_ref`
5//! shapes without mixing migration mechanics into the main contract module.
6
7use serde::Deserialize;
8use serde::de::DeserializeOwned;
9
10use super::{
11    CloudCreateSandboxRequest, CloudPullPolicy, CloudRootfsSource, CloudSandboxComputeResources,
12    CloudSandboxResources, CloudSandboxSpec,
13};
14
15type CompatResult<T> = Result<T, String>;
16
17impl<'de> Deserialize<'de> for CloudCreateSandboxRequest {
18    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
19    where
20        D: serde::Deserializer<'de>,
21    {
22        let value = serde_json::Value::deserialize(deserializer)?;
23        deserialize_request(value).map_err(serde::de::Error::custom)
24    }
25}
26
27fn deserialize_request(value: serde_json::Value) -> CompatResult<CloudCreateSandboxRequest> {
28    let object = value
29        .as_object()
30        .ok_or_else(|| "cloud sandbox create request must be an object".to_owned())?;
31    let has_image = object.contains_key("image");
32    let has_disk_snapshot = object.contains_key("disk_snapshot_ref");
33
34    if let Some(source) = object.get("source") {
35        let source = source
36            .as_str()
37            .ok_or_else(|| "cloud sandbox create source must be a string".to_owned())?
38            .to_owned();
39        let mut value = value;
40        request_object_mut(&mut value)?.remove("source");
41        return deserialize_tagged_request(value, &source);
42    }
43
44    match (has_image, has_disk_snapshot) {
45        (true, false) => deserialize_legacy_image_request(value),
46        (false, true) => deserialize_tagged_request(value, "disk_snapshot"),
47        (true, true) => Err("image and disk_snapshot_ref are mutually exclusive".to_owned()),
48        (false, false) => Err("exactly one of image and disk_snapshot_ref is required".to_owned()),
49    }
50}
51
52fn deserialize_tagged_request(
53    value: serde_json::Value,
54    source: &str,
55) -> CompatResult<CloudCreateSandboxRequest> {
56    match source {
57        "oci" => deserialize_oci_request(value),
58        "bind" => deserialize_bind_request(value),
59        "disk_image" => deserialize_disk_image_request(value),
60        "disk_snapshot" => deserialize_disk_snapshot_request(value),
61        unknown => Err(format!(
62            "unknown variant `{unknown}`, expected one of `oci`, `bind`, `disk_image`, `disk_snapshot`"
63        )),
64    }
65}
66
67fn deserialize_oci_request(
68    mut value: serde_json::Value,
69) -> CompatResult<CloudCreateSandboxRequest> {
70    let object = request_object_mut(&mut value)?;
71    let reference = take_required_field(object, "reference")?;
72    let resources = take_default_field(object, "resources")?;
73    let patches = take_default_field(object, "patches")?;
74    let pull_policy = take_default_field(object, "pull_policy")?;
75    let sandbox = deserialize_common_spec(value)?;
76
77    Ok(CloudCreateSandboxRequest::Oci {
78        sandbox,
79        reference,
80        resources,
81        patches,
82        pull_policy,
83    })
84}
85
86fn deserialize_bind_request(
87    mut value: serde_json::Value,
88) -> CompatResult<CloudCreateSandboxRequest> {
89    let object = request_object_mut(&mut value)?;
90    let path = take_required_field(object, "path")?;
91    let resources = take_compute_resources(object)?;
92    let patches = take_default_field(object, "patches")?;
93    let sandbox = deserialize_common_spec(value)?;
94
95    Ok(CloudCreateSandboxRequest::Bind {
96        sandbox,
97        path,
98        resources,
99        patches,
100    })
101}
102
103fn deserialize_disk_image_request(
104    mut value: serde_json::Value,
105) -> CompatResult<CloudCreateSandboxRequest> {
106    let object = request_object_mut(&mut value)?;
107    let path = take_required_field(object, "path")?;
108    let format = take_required_field(object, "format")?;
109    let fstype = take_default_field(object, "fstype")?;
110    let resources = take_compute_resources(object)?;
111    let patches = take_default_field(object, "patches")?;
112    let sandbox = deserialize_common_spec(value)?;
113
114    Ok(CloudCreateSandboxRequest::DiskImage {
115        sandbox,
116        path,
117        format,
118        fstype,
119        resources,
120        patches,
121    })
122}
123
124fn deserialize_disk_snapshot_request(
125    mut value: serde_json::Value,
126) -> CompatResult<CloudCreateSandboxRequest> {
127    let object = request_object_mut(&mut value)?;
128    let disk_snapshot_ref = take_required_field(object, "disk_snapshot_ref")?;
129    let resources = take_compute_resources(object)?;
130    let pull_policy = take_default_field(object, "pull_policy")?;
131    let sandbox = deserialize_common_spec(value)?;
132
133    Ok(CloudCreateSandboxRequest::DiskSnapshot {
134        sandbox,
135        disk_snapshot_ref,
136        resources,
137        pull_policy,
138    })
139}
140
141fn deserialize_legacy_image_request(
142    mut value: serde_json::Value,
143) -> CompatResult<CloudCreateSandboxRequest> {
144    let object = request_object_mut(&mut value)?;
145    let image: CloudRootfsSource = take_required_field(object, "image")?;
146    let resources: CloudSandboxResources = take_default_field(object, "resources")?;
147    let patches = take_default_field(object, "patches")?;
148    let pull_policy: CloudPullPolicy = take_default_field(object, "pull_policy")?;
149    let sandbox = deserialize_common_spec(value)?;
150
151    match image {
152        CloudRootfsSource::Oci { reference } => Ok(CloudCreateSandboxRequest::Oci {
153            sandbox,
154            reference,
155            resources,
156            patches,
157            pull_policy,
158        }),
159        CloudRootfsSource::Bind { path } => {
160            reject_non_oci_legacy_options(&resources, pull_policy)?;
161            Ok(CloudCreateSandboxRequest::Bind {
162                sandbox,
163                path,
164                resources: resources.into(),
165                patches,
166            })
167        }
168        CloudRootfsSource::DiskImage {
169            path,
170            format,
171            fstype,
172        } => {
173            reject_non_oci_legacy_options(&resources, pull_policy)?;
174            Ok(CloudCreateSandboxRequest::DiskImage {
175                sandbox,
176                path,
177                format,
178                fstype,
179                resources: resources.into(),
180                patches,
181            })
182        }
183    }
184}
185
186fn reject_non_oci_legacy_options(
187    resources: &CloudSandboxResources,
188    pull_policy: CloudPullPolicy,
189) -> CompatResult<()> {
190    if resources.disk_size_mib.is_some() {
191        return Err("resources.disk_size_mib is only valid for OCI source".to_owned());
192    }
193    if pull_policy != CloudPullPolicy::default() {
194        return Err("pull_policy is only valid for OCI and disk_snapshot sources".to_owned());
195    }
196    Ok(())
197}
198
199fn request_object_mut(
200    value: &mut serde_json::Value,
201) -> CompatResult<&mut serde_json::Map<String, serde_json::Value>> {
202    value
203        .as_object_mut()
204        .ok_or_else(|| "cloud sandbox create request must be an object".to_owned())
205}
206
207fn take_required_field<T: DeserializeOwned>(
208    object: &mut serde_json::Map<String, serde_json::Value>,
209    name: &'static str,
210) -> CompatResult<T> {
211    let value = object
212        .remove(name)
213        .ok_or_else(|| format!("missing field `{name}`"))?;
214    serde_json::from_value(value).map_err(|error| error.to_string())
215}
216
217fn take_default_field<T: Default + DeserializeOwned>(
218    object: &mut serde_json::Map<String, serde_json::Value>,
219    name: &'static str,
220) -> CompatResult<T> {
221    object
222        .remove(name)
223        .map(serde_json::from_value)
224        .transpose()
225        .map_err(|error| error.to_string())
226        .map(Option::unwrap_or_default)
227}
228
229fn take_compute_resources(
230    object: &mut serde_json::Map<String, serde_json::Value>,
231) -> CompatResult<CloudSandboxComputeResources> {
232    let resources: CloudSandboxResources = take_default_field(object, "resources")?;
233    if resources.disk_size_mib.is_some() {
234        return Err("resources.disk_size_mib is only valid for OCI source".to_owned());
235    }
236    Ok(resources.into())
237}
238
239fn deserialize_common_spec(value: serde_json::Value) -> CompatResult<CloudSandboxSpec> {
240    serde_json::from_value(value).map_err(|error| error.to_string())
241}