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            rate_limiter: None,
496            trust_host_cas: false,
497            outbound_proxy: None,
498        };
499        let runtime = SandboxRuntimeOptions {
500            workdir: self.runtime.workdir,
501            shell: self.runtime.shell,
502            scripts: self.runtime.scripts,
503            entrypoint: self.runtime.entrypoint,
504            cmd: self.runtime.cmd,
505            hostname: None,
506            user: self.runtime.user,
507            log_level: self.runtime.log_level,
508            metrics_sample_interval_ms: None,
509            disable_metrics_sample: false,
510        };
511
512        Ok(SandboxSpec {
513            name: self.name,
514            image,
515            resources,
516            runtime,
517            env: self.env,
518            labels: self.labels,
519            rlimits: self.rlimits.into_iter().map(Into::into).collect(),
520            mounts: self.mounts.into_iter().map(Into::into).collect(),
521            patches: patches.into_iter().map(Into::into).collect(),
522            network,
523            vsock: VsockSpec::default(),
524            init: self.init,
525            pull_policy: pull_policy.into(),
526            security_profile: self.security_profile,
527            deployment_profile: DeploymentProfile::default(),
528            lifecycle: self.lifecycle,
529        })
530    }
531}
532
533impl From<SandboxSpec> for CloudCreateSandboxRequest {
534    fn from(spec: SandboxSpec) -> Self {
535        let resources = CloudSandboxComputeResources {
536            vcpus: spec.resources.cpus,
537            memory_mib: spec.resources.memory_mib,
538        };
539        let patches = spec.patches.into_iter().map(Into::into).collect();
540        let pull_policy = spec.pull_policy.into();
541        let sandbox = CloudSandboxSpec {
542            name: spec.name,
543            runtime: CloudSandboxRuntimeOptions {
544                workdir: spec.runtime.workdir,
545                shell: spec.runtime.shell,
546                scripts: spec.runtime.scripts,
547                entrypoint: spec.runtime.entrypoint,
548                cmd: spec.runtime.cmd,
549                user: spec.runtime.user,
550                log_level: spec.runtime.log_level,
551            },
552            env: spec.env,
553            labels: spec.labels,
554            rlimits: spec.rlimits.into_iter().map(Into::into).collect(),
555            mounts: spec.mounts.into_iter().map(Into::into).collect(),
556            network: CloudNetworkSpec {
557                enabled: spec.network.enabled,
558                policy: spec.network.policy,
559                secrets: spec.network.secrets.map(Into::into),
560                strict: spec.network.strict,
561                max_tcp_connections: spec.network.max_tcp_connections,
562                max_udp_connections: spec.network.max_udp_connections,
563            },
564            init: spec.init,
565            security_profile: spec.security_profile,
566            lifecycle: spec.lifecycle,
567        };
568
569        match spec.image {
570            RootfsSource::Oci(oci) => Self::Oci {
571                sandbox,
572                reference: oci.reference,
573                resources: CloudSandboxResources {
574                    vcpus: resources.vcpus,
575                    memory_mib: resources.memory_mib,
576                    disk_size_mib: match oci.root_disk {
577                        Some(RootDisk::Managed { size_mib }) => size_mib,
578                        _ => None,
579                    },
580                },
581                patches,
582                pull_policy,
583            },
584            RootfsSource::Bind { path, .. } => Self::Bind {
585                sandbox,
586                path,
587                resources,
588                patches,
589            },
590            RootfsSource::DiskImage {
591                path,
592                format,
593                fstype,
594            } => Self::DiskImage {
595                sandbox,
596                path,
597                format: format.into(),
598                fstype,
599                resources,
600                patches,
601            },
602        }
603    }
604}
605
606impl Default for CloudSandboxResources {
607    fn default() -> Self {
608        let resources = SandboxResources::default();
609        Self {
610            vcpus: resources.cpus,
611            memory_mib: resources.memory_mib,
612            disk_size_mib: None,
613        }
614    }
615}
616
617impl Default for CloudSandboxComputeResources {
618    fn default() -> Self {
619        let resources = SandboxResources::default();
620        Self {
621            vcpus: resources.cpus,
622            memory_mib: resources.memory_mib,
623        }
624    }
625}
626
627impl From<CloudSandboxResources> for CloudSandboxComputeResources {
628    fn from(resources: CloudSandboxResources) -> Self {
629        Self {
630            vcpus: resources.vcpus,
631            memory_mib: resources.memory_mib,
632        }
633    }
634}
635
636impl Default for CloudCreateSandboxRequest {
637    fn default() -> Self {
638        Self::Oci {
639            sandbox: CloudSandboxSpec::default(),
640            reference: String::new(),
641            resources: CloudSandboxResources::default(),
642            patches: Vec::new(),
643            pull_policy: CloudPullPolicy::default(),
644        }
645    }
646}
647
648impl CloudRootfsSource {
649    /// Create an OCI rootfs source from an image reference.
650    pub fn oci(reference: impl Into<String>) -> Self {
651        Self::Oci {
652            reference: reference.into(),
653        }
654    }
655
656    /// Return the OCI image reference if this is an OCI rootfs.
657    pub fn oci_reference(&self) -> Option<&str> {
658        match self {
659            Self::Oci { reference } => Some(reference),
660            _ => None,
661        }
662    }
663}
664
665impl Default for CloudRootfsSource {
666    fn default() -> Self {
667        Self::oci(String::new())
668    }
669}
670
671#[cfg(test)]
672mod tests;