Skip to main content

microsandbox_types/cloud/
mod.rs

1//! Wire types for the cloud backend's HTTP calls.
2//!
3//! HTTP route versions choose this concrete request shape. The request shape is
4//! user-facing intent, so disk sizing sits beside CPU and memory; conversion
5//! into the domain spec moves that value onto the OCI rootfs where the runtime
6//! realizes it.
7//!
8//! Cloud request objects ignore unknown fields so SDK and server releases can
9//! evolve independently. Missing fields keep their documented defaults; known
10//! fields and enum variants are still validated. Acceptance of a request does
11//! not imply support for settings unknown to the receiving server.
12
13use std::collections::BTreeMap;
14use std::path::PathBuf;
15
16use chrono::{DateTime, Utc};
17use serde::{Deserialize, Serialize};
18
19use crate::domain::{
20    CpuPlacement, DeploymentProfile, EnvVar, HandoffInit, NetworkSpec, OciRootfsSource, RootDisk,
21    RootfsSource, SandboxPolicy, SandboxResources, SandboxRuntimeOptions, SandboxSpec,
22    SecurityProfile, TransparentHugePagePolicy, VsockSpec,
23};
24use crate::{TypesError, TypesResult};
25
26mod secrets;
27mod snapshots;
28mod specs;
29
30pub use secrets::{
31    CloudHostPattern, CloudSecretEntry, CloudSecretSource, CloudSecretsConfig, CloudViolationAction,
32};
33pub use snapshots::{
34    CloudCreateSnapshotRequest, CloudSnapshot, CloudSnapshotDetails, CloudSnapshotKind,
35    CloudSnapshotLocation, CloudSnapshotOperation, CloudSnapshotOperationStatus, CloudSnapshotSpec,
36};
37pub use specs::{
38    CloudDiskImageFormat, CloudNetworkSpec, CloudPatch, CloudPullPolicy, CloudRlimit,
39    CloudRlimitResource, CloudRootfsSource, CloudSandboxRuntimeOptions, CloudVolumeMount,
40};
41
42//--------------------------------------------------------------------------------------------------
43// Types: Request
44//--------------------------------------------------------------------------------------------------
45
46/// Wire shape of a cloud sandbox create request body.
47///
48/// Each root filesystem origin is a distinct source variant. The common
49/// sandbox settings remain flat beside the source-specific fields. Legacy
50/// requests carrying an `image` object are accepted during migration.
51#[derive(Debug, Clone, Serialize)]
52#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
53#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
54#[serde(tag = "source", rename_all = "snake_case")]
55pub enum CloudCreateSandboxRequest {
56    /// Create a sandbox from an OCI image.
57    Oci {
58        /// Settings shared by every sandbox source.
59        #[serde(flatten)]
60        sandbox: CloudSandboxSpec,
61        /// OCI image reference.
62        reference: String,
63        /// CPU, memory, and writable-disk resources.
64        #[serde(default)]
65        resources: CloudSandboxResources,
66        /// Rootfs patches applied before VM start.
67        #[serde(default)]
68        patches: Vec<CloudPatch>,
69        /// OCI image pull policy.
70        #[serde(default)]
71        pull_policy: CloudPullPolicy,
72    },
73    /// Create a sandbox from a host directory.
74    Bind {
75        /// Settings shared by every sandbox source.
76        #[serde(flatten)]
77        sandbox: CloudSandboxSpec,
78        /// Host directory used as the root filesystem.
79        #[cfg_attr(feature = "ts", ts(type = "string"))]
80        #[cfg_attr(feature = "utoipa", schema(value_type = String))]
81        path: PathBuf,
82        /// CPU and memory resources.
83        #[serde(default)]
84        resources: CloudSandboxComputeResources,
85        /// Rootfs patches applied before VM start.
86        #[serde(default)]
87        patches: Vec<CloudPatch>,
88    },
89    /// Create a sandbox from a disk image file.
90    DiskImage {
91        /// Settings shared by every sandbox source.
92        #[serde(flatten)]
93        sandbox: CloudSandboxSpec,
94        /// Host path to the disk image.
95        #[cfg_attr(feature = "ts", ts(type = "string"))]
96        #[cfg_attr(feature = "utoipa", schema(value_type = String))]
97        path: PathBuf,
98        /// Disk image format.
99        format: CloudDiskImageFormat,
100        /// Inner filesystem type, when it cannot be detected automatically.
101        fstype: Option<String>,
102        /// CPU and memory resources.
103        #[serde(default)]
104        resources: CloudSandboxComputeResources,
105        /// Rootfs patches applied before VM start.
106        #[serde(default)]
107        patches: Vec<CloudPatch>,
108    },
109    /// Create a fresh-booted sandbox from a disk snapshot.
110    DiskSnapshot {
111        /// Settings shared by every sandbox source.
112        #[serde(flatten)]
113        sandbox: CloudSandboxSpec,
114        /// Disk snapshot to restore.
115        disk_snapshot_ref: CloudSnapshotLocation,
116        /// CPU and memory resources.
117        #[serde(default)]
118        resources: CloudSandboxComputeResources,
119        /// Pull policy used if the snapshot's pinned base image must be fetched.
120        #[serde(default)]
121        pull_policy: CloudPullPolicy,
122    },
123}
124
125/// Settings shared by every cloud sandbox creation source.
126#[derive(Debug, Clone, Default, Serialize, Deserialize)]
127#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
128#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
129#[serde(default)]
130pub struct CloudSandboxSpec {
131    /// Unique sandbox name.
132    #[cfg_attr(feature = "utoipa", schema(required = true))]
133    pub name: String,
134
135    /// Guest runtime options.
136    pub runtime: CloudSandboxRuntimeOptions,
137
138    /// Environment variables visible to commands in the sandbox.
139    pub env: Vec<EnvVar>,
140
141    /// User-defined labels attached to the sandbox.
142    pub labels: BTreeMap<String, String>,
143
144    /// Sandbox-wide resource limits inherited by guest processes.
145    pub rlimits: Vec<CloudRlimit>,
146
147    /// Volume mounts.
148    pub mounts: Vec<CloudVolumeMount>,
149
150    /// Network specification.
151    pub network: CloudNetworkSpec,
152
153    /// Hand off PID 1 to a guest init binary after agentd setup.
154    pub init: Option<HandoffInit>,
155
156    /// In-guest security profile.
157    pub security_profile: SecurityProfile,
158
159    /// Sandbox lifecycle policy.
160    pub lifecycle: SandboxPolicy,
161}
162
163/// CPU and memory request shared by sources without a managed writable disk.
164#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
165#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
166#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
167#[serde(default)]
168pub struct CloudSandboxComputeResources {
169    /// Number of virtual CPUs.
170    pub vcpus: u8,
171
172    /// Guest memory in MiB.
173    pub memory_mib: u32,
174}
175
176/// Cloud resource request.
177#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
178#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
179#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
180#[serde(default)]
181pub struct CloudSandboxResources {
182    /// Number of virtual CPUs.
183    pub vcpus: u8,
184
185    /// Guest memory in MiB.
186    pub memory_mib: u32,
187
188    /// Writable disk size in MiB. Applies only to OCI root filesystems.
189    #[serde(default, skip_serializing_if = "Option::is_none")]
190    pub disk_size_mib: Option<u32>,
191}
192
193//--------------------------------------------------------------------------------------------------
194// Types: Response
195//--------------------------------------------------------------------------------------------------
196
197/// Wire shape of the cloud sandbox response returned by sandbox endpoints.
198#[derive(Debug, Clone, Serialize, Deserialize)]
199#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
200#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
201pub struct CloudCreateSandboxResponse {
202    /// Server-side UUID.
203    pub id: String,
204    /// Owning org's UUID.
205    pub org_id: String,
206    /// User-facing, per-org sandbox name.
207    pub name: String,
208    /// Canonical, resolved SSH username token.
209    pub slug: String,
210    /// Current lifecycle status.
211    pub status: CloudSandboxStatus,
212    /// Why the sandbox is not running yet, when known. Only present while
213    /// `status` is `starting`.
214    #[serde(default)]
215    pub status_reason: Option<CloudSandboxStatusReason>,
216    /// Curated resolved-spec projection returned by the control plane, when
217    /// available. Lifecycle and agent operations intentionally do not depend
218    /// on reconstructing the create request from this server-owned view.
219    #[serde(default, skip_serializing_if = "Option::is_none")]
220    #[cfg_attr(feature = "ts", ts(type = "unknown | null | undefined"))]
221    pub spec: Option<serde_json::Value>,
222    /// Whether the sandbox should be removed when its allocation terminates.
223    pub ephemeral: bool,
224    /// Creation timestamp.
225    #[cfg_attr(feature = "ts", ts(type = "string"))]
226    pub created_at: DateTime<Utc>,
227    /// Last start timestamp, when known.
228    #[serde(default)]
229    #[cfg_attr(feature = "ts", ts(type = "string | null"))]
230    pub started_at: Option<DateTime<Utc>>,
231    /// Last stop timestamp, when known.
232    #[serde(default)]
233    #[cfg_attr(feature = "ts", ts(type = "string | null"))]
234    pub stopped_at: Option<DateTime<Utc>>,
235    /// Human-readable message for the most recent failure, when any.
236    #[serde(default)]
237    pub last_failure_message: Option<String>,
238}
239
240/// Sandbox lifecycle status returned by the cloud control plane.
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
242#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
243#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
244#[serde(rename_all = "snake_case")]
245pub enum CloudSandboxStatus {
246    /// Created in the database but not yet started.
247    Created,
248    /// Start request has been submitted.
249    Starting,
250    /// Sandbox is running.
251    Running,
252    /// Stop request has been submitted.
253    Stopping,
254    /// Sandbox is stopped.
255    Stopped,
256    /// Sandbox failed.
257    Failed,
258}
259
260/// Reason a sandbox start is still in progress. Only meaningful while
261/// `status` is `starting`.
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
263#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
264#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
265#[serde(rename_all = "snake_case")]
266pub enum CloudSandboxStatusReason {
267    /// The start has been accepted and is being scheduled.
268    Scheduling,
269    /// No capacity is currently available; the start proceeds when
270    /// capacity frees up.
271    InsufficientCapacity,
272}
273
274/// Wire shape of paginated list responses.
275#[derive(Debug, Clone, Serialize, Deserialize)]
276#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
277#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
278pub struct CloudPaginated<T> {
279    /// Page of response items.
280    pub data: Vec<T>,
281    /// Cursor for the next page, when one exists.
282    #[serde(default)]
283    pub next_cursor: Option<String>,
284}
285
286/// Wire shape of the message response returned by mutation endpoints.
287#[derive(Debug, Clone, Serialize, Deserialize)]
288#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
289#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
290pub struct CloudMessageResponse {
291    /// Human-readable response message.
292    pub message: String,
293}
294
295/// Wire shape of the typed error body returned by cloud APIs on 4xx/5xx responses.
296#[derive(Debug, Clone, Serialize, Deserialize)]
297#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
298#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
299pub struct CloudErrorBody {
300    /// Flat machine-readable error code, when returned in this shape.
301    #[serde(default)]
302    pub code: Option<String>,
303    /// Flat human-readable error message, when returned in this shape.
304    #[serde(default)]
305    pub message: Option<String>,
306    /// Nested error object returned by the API error responder.
307    #[serde(default)]
308    pub error: Option<CloudErrorDetails>,
309}
310
311/// Nested cloud API error details.
312#[derive(Debug, Clone, Serialize, Deserialize)]
313#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
314#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
315pub struct CloudErrorDetails {
316    /// Machine-readable error code.
317    #[serde(default)]
318    pub code: Option<String>,
319    /// Human-readable error message.
320    #[serde(default)]
321    pub message: Option<String>,
322}
323
324//--------------------------------------------------------------------------------------------------
325// Trait Implementations
326//--------------------------------------------------------------------------------------------------
327
328impl TryFrom<CloudCreateSandboxRequest> for SandboxSpec {
329    type Error = TypesError;
330
331    fn try_from(req: CloudCreateSandboxRequest) -> TypesResult<Self> {
332        match req {
333            CloudCreateSandboxRequest::Oci {
334                sandbox,
335                reference,
336                resources,
337                patches,
338                pull_policy,
339            } => sandbox.into_domain_spec(
340                RootfsSource::Oci(OciRootfsSource {
341                    reference,
342                    root_disk: resources.disk_size_mib.map(RootDisk::managed),
343                }),
344                resources.into(),
345                patches,
346                pull_policy,
347            ),
348            CloudCreateSandboxRequest::Bind {
349                sandbox,
350                path,
351                resources,
352                patches,
353            } => sandbox.into_domain_spec(
354                RootfsSource::Bind {
355                    path,
356                    follow_root_symlinks: false,
357                },
358                resources,
359                patches,
360                CloudPullPolicy::default(),
361            ),
362            CloudCreateSandboxRequest::DiskImage {
363                sandbox,
364                path,
365                format,
366                fstype,
367                resources,
368                patches,
369            } => sandbox.into_domain_spec(
370                RootfsSource::DiskImage {
371                    path,
372                    format: format.into(),
373                    fstype,
374                },
375                resources,
376                patches,
377                CloudPullPolicy::default(),
378            ),
379            CloudCreateSandboxRequest::DiskSnapshot { .. } => Err(TypesError::invalid_config(
380                "disk_snapshot_ref is not supported here: resolve the snapshot reference \
381                 to a concrete image before converting to a sandbox spec",
382            )),
383        }
384    }
385}
386
387impl CloudCreateSandboxRequest {
388    /// Return settings shared by every sandbox source.
389    pub const fn sandbox_spec(&self) -> &CloudSandboxSpec {
390        match self {
391            Self::Oci { sandbox, .. }
392            | Self::Bind { sandbox, .. }
393            | Self::DiskImage { sandbox, .. }
394            | Self::DiskSnapshot { sandbox, .. } => sandbox,
395        }
396    }
397
398    /// Return mutable settings shared by every sandbox source.
399    pub const fn sandbox_spec_mut(&mut self) -> &mut CloudSandboxSpec {
400        match self {
401            Self::Oci { sandbox, .. }
402            | Self::Bind { sandbox, .. }
403            | Self::DiskImage { sandbox, .. }
404            | Self::DiskSnapshot { sandbox, .. } => sandbox,
405        }
406    }
407
408    /// Return the disk snapshot reference, when restoring one.
409    pub const fn disk_snapshot_ref(&self) -> Option<&CloudSnapshotLocation> {
410        match self {
411            Self::DiskSnapshot {
412                disk_snapshot_ref, ..
413            } => Some(disk_snapshot_ref),
414            _ => None,
415        }
416    }
417
418    /// Return the OCI image reference, when creating from OCI.
419    pub fn oci_reference(&self) -> Option<&str> {
420        match self {
421            Self::Oci { reference, .. } => Some(reference),
422            _ => None,
423        }
424    }
425
426    /// Return the requested CPU and memory resources.
427    pub const fn compute_resources(&self) -> CloudSandboxComputeResources {
428        match self {
429            Self::Oci { resources, .. } => CloudSandboxComputeResources {
430                vcpus: resources.vcpus,
431                memory_mib: resources.memory_mib,
432            },
433            Self::Bind { resources, .. }
434            | Self::DiskImage { resources, .. }
435            | Self::DiskSnapshot { resources, .. } => *resources,
436        }
437    }
438
439    /// Return the requested OCI writable-disk size, if this is an OCI source.
440    pub const fn oci_disk_size_mib(&self) -> Option<Option<u32>> {
441        match self {
442            Self::Oci { resources, .. } => Some(resources.disk_size_mib),
443            _ => None,
444        }
445    }
446
447    /// Set the OCI writable-disk size, returning whether this is an OCI source.
448    pub fn set_oci_disk_size_mib(&mut self, disk_size_mib: u32) -> bool {
449        let Self::Oci { resources, .. } = self else {
450            return false;
451        };
452        resources.disk_size_mib = Some(disk_size_mib);
453        true
454    }
455}
456
457impl CloudSandboxSpec {
458    fn into_domain_spec(
459        self,
460        image: RootfsSource,
461        resources: CloudSandboxComputeResources,
462        patches: Vec<CloudPatch>,
463        pull_policy: CloudPullPolicy,
464    ) -> TypesResult<SandboxSpec> {
465        let resources = SandboxResources {
466            cpus: resources.vcpus,
467            memory_mib: resources.memory_mib,
468            // The cloud wire type has no boot-capacity fields yet; treat the
469            // effective resources as the maximum (mirrors SandboxResources
470            // deserialization for legacy configs).
471            max_cpus: resources.vcpus,
472            max_memory_mib: resources.memory_mib,
473            // Host runtime policy never crosses the cloud wire. A managed
474            // service applies its own placement and guest-memory defaults
475            // after resolving the tenant-controlled resource request.
476            cpu_placement: CpuPlacement::Inherit,
477            placement_profile: None,
478            thp: TransparentHugePagePolicy::Madvise,
479        };
480
481        // Fields not present on `CloudNetworkSpec` are defaulted here, listed
482        // explicitly (not `..default()`) so a new `NetworkSpec` field forces a
483        // decision here.
484        let network = NetworkSpec {
485            enabled: self.network.enabled,
486            interface: None,
487            ports: Vec::new(),
488            policy: self.network.policy,
489            dns: None,
490            tls: None,
491            strict: self.network.strict,
492            secrets: self.network.secrets.map(Into::into),
493            max_tcp_connections: self.network.max_tcp_connections,
494            max_udp_connections: self.network.max_udp_connections,
495            // Tunes published-port listeners, and the cloud spec publishes no ports.
496            tcp_accept_queue_size: None,
497            rate_limiter: None,
498            nat64_prefixes: NetworkSpec::default().nat64_prefixes,
499            trust_host_cas: false,
500            http: Default::default(),
501            outbound_proxy: None,
502        };
503        let runtime = SandboxRuntimeOptions {
504            workdir: self.runtime.workdir,
505            shell: self.runtime.shell,
506            scripts: self.runtime.scripts,
507            entrypoint: self.runtime.entrypoint,
508            cmd: self.runtime.cmd,
509            hostname: None,
510            user: self.runtime.user,
511            log_level: self.runtime.log_level,
512            metrics_sample_interval_ms: None,
513            disable_metrics_sample: false,
514            guest_clock: None,
515        };
516
517        Ok(SandboxSpec {
518            name: self.name,
519            image,
520            resources,
521            runtime,
522            env: self.env,
523            labels: self.labels,
524            rlimits: self.rlimits.into_iter().map(Into::into).collect(),
525            mounts: self.mounts.into_iter().map(Into::into).collect(),
526            patches: patches.into_iter().map(Into::into).collect(),
527            network,
528            vsock: VsockSpec::default(),
529            init: self.init,
530            pull_policy: pull_policy.into(),
531            security_profile: self.security_profile,
532            deployment_profile: DeploymentProfile::default(),
533            lifecycle: self.lifecycle,
534        })
535    }
536}
537
538impl From<SandboxSpec> for CloudCreateSandboxRequest {
539    fn from(spec: SandboxSpec) -> Self {
540        let resources = CloudSandboxComputeResources {
541            vcpus: spec.resources.cpus,
542            memory_mib: spec.resources.memory_mib,
543        };
544        let patches = spec.patches.into_iter().map(Into::into).collect();
545        let pull_policy = spec.pull_policy.into();
546        let sandbox = CloudSandboxSpec {
547            name: spec.name,
548            runtime: CloudSandboxRuntimeOptions {
549                workdir: spec.runtime.workdir,
550                shell: spec.runtime.shell,
551                scripts: spec.runtime.scripts,
552                entrypoint: spec.runtime.entrypoint,
553                cmd: spec.runtime.cmd,
554                user: spec.runtime.user,
555                log_level: spec.runtime.log_level,
556            },
557            env: spec.env,
558            labels: spec.labels,
559            rlimits: spec.rlimits.into_iter().map(Into::into).collect(),
560            mounts: spec.mounts.into_iter().map(Into::into).collect(),
561            network: CloudNetworkSpec {
562                enabled: spec.network.enabled,
563                policy: spec.network.policy,
564                secrets: spec.network.secrets.map(Into::into),
565                strict: spec.network.strict,
566                max_tcp_connections: spec.network.max_tcp_connections,
567                max_udp_connections: spec.network.max_udp_connections,
568            },
569            init: spec.init,
570            security_profile: spec.security_profile,
571            lifecycle: spec.lifecycle,
572        };
573
574        match spec.image {
575            RootfsSource::Oci(oci) => Self::Oci {
576                sandbox,
577                reference: oci.reference,
578                resources: CloudSandboxResources {
579                    vcpus: resources.vcpus,
580                    memory_mib: resources.memory_mib,
581                    disk_size_mib: match oci.root_disk {
582                        Some(RootDisk::Managed { size_mib }) => size_mib,
583                        _ => None,
584                    },
585                },
586                patches,
587                pull_policy,
588            },
589            RootfsSource::Bind { path, .. } => Self::Bind {
590                sandbox,
591                path,
592                resources,
593                patches,
594            },
595            RootfsSource::DiskImage {
596                path,
597                format,
598                fstype,
599            } => Self::DiskImage {
600                sandbox,
601                path,
602                format: format.into(),
603                fstype,
604                resources,
605                patches,
606            },
607        }
608    }
609}
610
611impl Default for CloudSandboxResources {
612    fn default() -> Self {
613        let resources = SandboxResources::default();
614        Self {
615            vcpus: resources.cpus,
616            memory_mib: resources.memory_mib,
617            disk_size_mib: None,
618        }
619    }
620}
621
622impl Default for CloudSandboxComputeResources {
623    fn default() -> Self {
624        let resources = SandboxResources::default();
625        Self {
626            vcpus: resources.cpus,
627            memory_mib: resources.memory_mib,
628        }
629    }
630}
631
632impl From<CloudSandboxResources> for CloudSandboxComputeResources {
633    fn from(resources: CloudSandboxResources) -> Self {
634        Self {
635            vcpus: resources.vcpus,
636            memory_mib: resources.memory_mib,
637        }
638    }
639}
640
641impl Default for CloudCreateSandboxRequest {
642    fn default() -> Self {
643        Self::Oci {
644            sandbox: CloudSandboxSpec::default(),
645            reference: String::new(),
646            resources: CloudSandboxResources::default(),
647            patches: Vec::new(),
648            pull_policy: CloudPullPolicy::default(),
649        }
650    }
651}
652
653impl CloudRootfsSource {
654    /// Create an OCI rootfs source from an image reference.
655    pub fn oci(reference: impl Into<String>) -> Self {
656        Self::Oci {
657            reference: reference.into(),
658        }
659    }
660
661    /// Return the OCI image reference if this is an OCI rootfs.
662    pub fn oci_reference(&self) -> Option<&str> {
663        match self {
664            Self::Oci { reference } => Some(reference),
665            _ => None,
666        }
667    }
668}
669
670impl Default for CloudRootfsSource {
671    fn default() -> Self {
672        Self::oci(String::new())
673    }
674}
675
676#[cfg(test)]
677mod tests;