Skip to main content

microsandbox_types/compat/
cloud.rs

1//! Version-neutral deserialization for current and previous cloud requests.
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 and previous secret fields. Current payloads are read directly;
6//! previous field conversion is delegated to the corresponding version module.
7
8use std::path::PathBuf;
9
10use serde::{Deserialize, Deserializer};
11
12use crate::cloud::{
13    CloudCreateSandboxRequest, CloudDiskImageFormat, CloudHostPattern, CloudPatch, CloudPullPolicy,
14    CloudSandboxResources, CloudSandboxSpec, CloudSecretEntry, CloudSecretSource,
15    CloudSecretsConfig, CloudSnapshotLocation, CloudViolationAction,
16};
17use crate::compat::field::Field;
18use crate::compat::v0_5_0::local::secrets::SecretInjection;
19use crate::compat::v0_6_7::cloud::secrets;
20use crate::{SecretSubstitution, compat};
21
22//--------------------------------------------------------------------------------------------------
23// Types
24//--------------------------------------------------------------------------------------------------
25
26#[derive(Deserialize)]
27struct SecretsConfig {
28    #[serde(default)]
29    entries: Vec<CloudSecretEntry>,
30    #[serde(default)]
31    passthrough_hosts: Field<Option<Vec<CloudHostPattern>>>,
32    #[serde(default)]
33    violation_action: Field<CloudViolationAction>,
34    #[serde(default)]
35    on_violation: Field<secrets::ViolationAction>,
36}
37
38#[derive(Deserialize)]
39struct SecretEntry {
40    env_var: String,
41    #[serde(default)]
42    value: String,
43    source: Option<CloudSecretSource>,
44    placeholder: String,
45    #[serde(default)]
46    allowed_hosts: Vec<CloudHostPattern>,
47    #[serde(default)]
48    substitution: Field<SecretSubstitution>,
49    #[serde(default)]
50    injection: Field<SecretInjection>,
51    #[serde(default)]
52    passthrough_hosts: Field<Vec<CloudHostPattern>>,
53    #[serde(default)]
54    violation_action: Field<Option<CloudViolationAction>>,
55    #[serde(default)]
56    on_violation: Field<Option<secrets::ViolationAction>>,
57    #[serde(default = "default_true")]
58    require_tls_identity: bool,
59}
60
61#[derive(Deserialize)]
62#[serde(untagged)]
63enum CreateRequest {
64    Tagged(TaggedRequest),
65    PreviousImage(PreviousImageRequest),
66    PreviousSnapshot(PreviousSnapshotRequest),
67}
68
69#[derive(Deserialize)]
70#[serde(tag = "source", rename_all = "snake_case")]
71enum TaggedRequest {
72    Oci {
73        #[serde(flatten)]
74        sandbox: CloudSandboxSpec,
75        reference: String,
76        #[serde(default)]
77        resources: CloudSandboxResources,
78        #[serde(default)]
79        patches: Vec<CloudPatch>,
80        #[serde(default)]
81        pull_policy: CloudPullPolicy,
82    },
83    Bind {
84        #[serde(flatten)]
85        sandbox: CloudSandboxSpec,
86        path: PathBuf,
87        #[serde(default)]
88        resources: CloudSandboxResources,
89        #[serde(default)]
90        patches: Vec<CloudPatch>,
91    },
92    DiskImage {
93        #[serde(flatten)]
94        sandbox: CloudSandboxSpec,
95        path: PathBuf,
96        format: CloudDiskImageFormat,
97        fstype: Option<String>,
98        #[serde(default)]
99        resources: CloudSandboxResources,
100        #[serde(default)]
101        patches: Vec<CloudPatch>,
102    },
103    DiskSnapshot {
104        #[serde(flatten)]
105        sandbox: CloudSandboxSpec,
106        disk_snapshot_ref: CloudSnapshotLocation,
107        #[serde(default)]
108        resources: CloudSandboxResources,
109        #[serde(default)]
110        pull_policy: CloudPullPolicy,
111    },
112}
113
114#[derive(Deserialize)]
115struct PreviousImageRequest {
116    #[serde(default)]
117    source: Field<serde::de::IgnoredAny>,
118    #[serde(default)]
119    disk_snapshot_ref: Field<serde::de::IgnoredAny>,
120    #[serde(flatten)]
121    request: compat::v0_6_5::cloud::create::CreateRequest,
122}
123
124#[derive(Deserialize)]
125struct PreviousSnapshotRequest {
126    #[serde(default)]
127    source: Field<serde::de::IgnoredAny>,
128    #[serde(default)]
129    image: Field<serde::de::IgnoredAny>,
130    disk_snapshot_ref: CloudSnapshotLocation,
131    #[serde(default)]
132    resources: CloudSandboxResources,
133    #[serde(default)]
134    pull_policy: CloudPullPolicy,
135    #[serde(flatten)]
136    sandbox: CloudSandboxSpec,
137}
138
139//--------------------------------------------------------------------------------------------------
140// Methods
141//--------------------------------------------------------------------------------------------------
142
143impl CreateRequest {
144    fn into_current(self) -> Result<CloudCreateSandboxRequest, String> {
145        let tagged = match self {
146            Self::Tagged(request) => request,
147            Self::PreviousImage(request) => {
148                reject_tagged_fallback(request.source)?;
149                if matches!(request.disk_snapshot_ref, Field::Present(_)) {
150                    return Err("image and disk_snapshot_ref are mutually exclusive".into());
151                }
152                return request.request.try_into();
153            }
154            Self::PreviousSnapshot(request) => {
155                reject_tagged_fallback(request.source)?;
156                if matches!(request.image, Field::Present(_)) {
157                    return Err("image and disk_snapshot_ref are mutually exclusive".into());
158                }
159                TaggedRequest::DiskSnapshot {
160                    sandbox: request.sandbox,
161                    disk_snapshot_ref: request.disk_snapshot_ref,
162                    resources: request.resources,
163                    pull_policy: request.pull_policy,
164                }
165            }
166        };
167        Ok(match tagged {
168            TaggedRequest::Oci {
169                sandbox,
170                reference,
171                resources,
172                patches,
173                pull_policy,
174            } => CloudCreateSandboxRequest::Oci {
175                sandbox,
176                reference,
177                resources,
178                patches,
179                pull_policy,
180            },
181            TaggedRequest::Bind {
182                sandbox,
183                path,
184                resources,
185                patches,
186            } => {
187                reject_disk_size(&resources)?;
188                CloudCreateSandboxRequest::Bind {
189                    sandbox,
190                    path,
191                    resources: resources.into(),
192                    patches,
193                }
194            }
195            TaggedRequest::DiskImage {
196                sandbox,
197                path,
198                format,
199                fstype,
200                resources,
201                patches,
202            } => {
203                reject_disk_size(&resources)?;
204                CloudCreateSandboxRequest::DiskImage {
205                    sandbox,
206                    path,
207                    format,
208                    fstype,
209                    resources: resources.into(),
210                    patches,
211                }
212            }
213            TaggedRequest::DiskSnapshot {
214                sandbox,
215                disk_snapshot_ref,
216                resources,
217                pull_policy,
218            } => {
219                reject_disk_size(&resources)?;
220                CloudCreateSandboxRequest::DiskSnapshot {
221                    sandbox,
222                    disk_snapshot_ref,
223                    resources: resources.into(),
224                    pull_policy,
225                }
226            }
227        })
228    }
229}
230
231//--------------------------------------------------------------------------------------------------
232// Trait Implementations
233//--------------------------------------------------------------------------------------------------
234
235impl<'de> Deserialize<'de> for CloudCreateSandboxRequest {
236    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
237    where
238        D: serde::Deserializer<'de>,
239    {
240        CreateRequest::deserialize(deserializer)?
241            .into_current()
242            .map_err(serde::de::Error::custom)
243    }
244}
245
246//--------------------------------------------------------------------------------------------------
247// Functions
248//--------------------------------------------------------------------------------------------------
249
250pub(crate) fn deserialize_secrets_config<'de, D: Deserializer<'de>>(
251    deserializer: D,
252) -> Result<CloudSecretsConfig, D::Error> {
253    let config = SecretsConfig::deserialize(deserializer).map_err(secret_error)?;
254    let (violation_action, on_violation_hosts) =
255        match (config.violation_action, config.on_violation) {
256            (Field::Present(_), Field::Present(_)) => {
257                return Err(secret_error("conflicting policies"));
258            }
259            (Field::Present(action), Field::Missing) => (Some(action), None),
260            (Field::Missing, Field::Present(action)) => action.into_current(),
261            (Field::Missing, Field::Missing) => (None, None),
262        };
263    let passthrough_hosts = match (config.passthrough_hosts, on_violation_hosts) {
264        (Field::Present(_), Some(_)) => return Err(secret_error("conflicting passthrough")),
265        (Field::Present(hosts), None) => hosts,
266        (Field::Missing, hosts) => hosts,
267    };
268    Ok(CloudSecretsConfig {
269        entries: config.entries,
270        passthrough_hosts,
271        violation_action: violation_action.unwrap_or_default(),
272    })
273}
274
275pub(crate) fn deserialize_secret_entry<'de, D: Deserializer<'de>>(
276    deserializer: D,
277) -> Result<CloudSecretEntry, D::Error> {
278    let entry = SecretEntry::deserialize(deserializer).map_err(secret_error)?;
279    let substitution = match (entry.substitution, entry.injection) {
280        (Field::Present(_), Field::Present(_)) => return Err(secret_error("conflicting scopes")),
281        (Field::Present(scopes), Field::Missing) => scopes,
282        (Field::Missing, Field::Present(scopes)) => scopes.into(),
283        (Field::Missing, Field::Missing) => SecretSubstitution::default(),
284    };
285    let (violation_action, on_violation_hosts) = match (entry.violation_action, entry.on_violation)
286    {
287        (Field::Present(_), Field::Present(_)) => return Err(secret_error("conflicting policies")),
288        (Field::Present(action), Field::Missing) => (action, None),
289        (Field::Missing, Field::Present(Some(action))) => action.into_current(),
290        (Field::Missing, Field::Present(None) | Field::Missing) => (None, None),
291    };
292    let passthrough_hosts = match (entry.passthrough_hosts, on_violation_hosts) {
293        (Field::Present(_), Some(_)) => return Err(secret_error("conflicting passthrough")),
294        (Field::Present(hosts), None) => hosts,
295        (Field::Missing, hosts) => hosts.unwrap_or_default(),
296    };
297    Ok(CloudSecretEntry {
298        env_var: entry.env_var,
299        value: entry.value,
300        source: entry.source,
301        placeholder: entry.placeholder,
302        allowed_hosts: entry.allowed_hosts,
303        substitution,
304        passthrough_hosts,
305        violation_action,
306        require_tls_identity: entry.require_tls_identity,
307    })
308}
309
310fn default_true() -> bool {
311    true
312}
313
314// Keep diagnostics independent of secret-bearing caller input.
315fn secret_error<E: serde::de::Error>(_: impl std::fmt::Display) -> E {
316    E::custom("invalid cloud secret configuration")
317}
318
319fn reject_disk_size(resources: &CloudSandboxResources) -> Result<(), String> {
320    if resources.disk_size_mib.is_some() {
321        return Err("resources.disk_size_mib is only valid for OCI source".into());
322    }
323    Ok(())
324}
325
326fn reject_tagged_fallback(source: Field<serde::de::IgnoredAny>) -> Result<(), String> {
327    if matches!(source, Field::Present(_)) {
328        return Err("invalid tagged cloud create request".into());
329    }
330    Ok(())
331}