1use 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 compat;
27mod secrets;
28mod snapshots;
29mod specs;
30
31pub use secrets::{
32 CloudHostPattern, CloudSecretEntry, CloudSecretSource, CloudSecretsConfig, CloudViolationAction,
33};
34pub use snapshots::{
35 CloudCreateSnapshotRequest, CloudSnapshot, CloudSnapshotDetails, CloudSnapshotKind,
36 CloudSnapshotLocation, CloudSnapshotOperation, CloudSnapshotOperationStatus, CloudSnapshotSpec,
37};
38pub use specs::{
39 CloudDiskImageFormat, CloudNetworkSpec, CloudPatch, CloudPullPolicy, CloudRlimit,
40 CloudRlimitResource, CloudRootfsSource, CloudSandboxRuntimeOptions, CloudVolumeMount,
41};
42
43#[derive(Debug, Clone, Serialize)]
53#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
54#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
55#[serde(tag = "source", rename_all = "snake_case")]
56pub enum CloudCreateSandboxRequest {
57 Oci {
59 #[serde(flatten)]
61 sandbox: CloudSandboxSpec,
62 reference: String,
64 #[serde(default)]
66 resources: CloudSandboxResources,
67 #[serde(default)]
69 patches: Vec<CloudPatch>,
70 #[serde(default)]
72 pull_policy: CloudPullPolicy,
73 },
74 Bind {
76 #[serde(flatten)]
78 sandbox: CloudSandboxSpec,
79 #[cfg_attr(feature = "ts", ts(type = "string"))]
81 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
82 path: PathBuf,
83 #[serde(default)]
85 resources: CloudSandboxComputeResources,
86 #[serde(default)]
88 patches: Vec<CloudPatch>,
89 },
90 DiskImage {
92 #[serde(flatten)]
94 sandbox: CloudSandboxSpec,
95 #[cfg_attr(feature = "ts", ts(type = "string"))]
97 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
98 path: PathBuf,
99 format: CloudDiskImageFormat,
101 fstype: Option<String>,
103 #[serde(default)]
105 resources: CloudSandboxComputeResources,
106 #[serde(default)]
108 patches: Vec<CloudPatch>,
109 },
110 DiskSnapshot {
112 #[serde(flatten)]
114 sandbox: CloudSandboxSpec,
115 disk_snapshot_ref: CloudSnapshotLocation,
117 #[serde(default)]
119 resources: CloudSandboxComputeResources,
120 #[serde(default)]
122 pull_policy: CloudPullPolicy,
123 },
124}
125
126#[derive(Debug, Clone, Default, Serialize, Deserialize)]
128#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
129#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
130#[serde(default)]
131pub struct CloudSandboxSpec {
132 #[cfg_attr(feature = "utoipa", schema(required = true))]
134 pub name: String,
135
136 pub runtime: CloudSandboxRuntimeOptions,
138
139 pub env: Vec<EnvVar>,
141
142 pub labels: BTreeMap<String, String>,
144
145 pub rlimits: Vec<CloudRlimit>,
147
148 pub mounts: Vec<CloudVolumeMount>,
150
151 pub network: CloudNetworkSpec,
153
154 pub init: Option<HandoffInit>,
156
157 pub security_profile: SecurityProfile,
159
160 pub lifecycle: SandboxPolicy,
162}
163
164#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
166#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
167#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
168#[serde(default)]
169pub struct CloudSandboxComputeResources {
170 pub vcpus: u8,
172
173 pub memory_mib: u32,
175}
176
177#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
179#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
180#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
181#[serde(default)]
182pub struct CloudSandboxResources {
183 pub vcpus: u8,
185
186 pub memory_mib: u32,
188
189 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub disk_size_mib: Option<u32>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
200#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
201#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
202pub struct CloudCreateSandboxResponse {
203 pub id: String,
205 pub org_id: String,
207 pub name: String,
209 pub slug: String,
211 pub status: CloudSandboxStatus,
213 #[serde(default)]
216 pub status_reason: Option<CloudSandboxStatusReason>,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
221 #[cfg_attr(feature = "ts", ts(type = "unknown | null | undefined"))]
222 pub spec: Option<serde_json::Value>,
223 pub ephemeral: bool,
225 #[cfg_attr(feature = "ts", ts(type = "string"))]
227 pub created_at: DateTime<Utc>,
228 #[serde(default)]
230 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
231 pub started_at: Option<DateTime<Utc>>,
232 #[serde(default)]
234 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
235 pub stopped_at: Option<DateTime<Utc>>,
236 #[serde(default)]
238 pub last_failure_message: Option<String>,
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
243#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
244#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
245#[serde(rename_all = "snake_case")]
246pub enum CloudSandboxStatus {
247 Created,
249 Starting,
251 Running,
253 Stopping,
255 Stopped,
257 Failed,
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
264#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
265#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
266#[serde(rename_all = "snake_case")]
267pub enum CloudSandboxStatusReason {
268 Scheduling,
270 InsufficientCapacity,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
278#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
279pub struct CloudPaginated<T> {
280 pub data: Vec<T>,
282 #[serde(default)]
284 pub next_cursor: Option<String>,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
289#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
290#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
291pub struct CloudMessageResponse {
292 pub message: String,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
298#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
299#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
300pub struct CloudErrorBody {
301 #[serde(default)]
303 pub code: Option<String>,
304 #[serde(default)]
306 pub message: Option<String>,
307 #[serde(default)]
309 pub error: Option<CloudErrorDetails>,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
314#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
315#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
316pub struct CloudErrorDetails {
317 #[serde(default)]
319 pub code: Option<String>,
320 #[serde(default)]
322 pub message: Option<String>,
323}
324
325impl TryFrom<CloudCreateSandboxRequest> for SandboxSpec {
330 type Error = TypesError;
331
332 fn try_from(req: CloudCreateSandboxRequest) -> TypesResult<Self> {
333 match req {
334 CloudCreateSandboxRequest::Oci {
335 sandbox,
336 reference,
337 resources,
338 patches,
339 pull_policy,
340 } => sandbox.into_domain_spec(
341 RootfsSource::Oci(OciRootfsSource {
342 reference,
343 root_disk: resources.disk_size_mib.map(RootDisk::managed),
344 }),
345 resources.into(),
346 patches,
347 pull_policy,
348 ),
349 CloudCreateSandboxRequest::Bind {
350 sandbox,
351 path,
352 resources,
353 patches,
354 } => sandbox.into_domain_spec(
355 RootfsSource::Bind {
356 path,
357 follow_root_symlinks: false,
358 },
359 resources,
360 patches,
361 CloudPullPolicy::default(),
362 ),
363 CloudCreateSandboxRequest::DiskImage {
364 sandbox,
365 path,
366 format,
367 fstype,
368 resources,
369 patches,
370 } => sandbox.into_domain_spec(
371 RootfsSource::DiskImage {
372 path,
373 format: format.into(),
374 fstype,
375 },
376 resources,
377 patches,
378 CloudPullPolicy::default(),
379 ),
380 CloudCreateSandboxRequest::DiskSnapshot { .. } => Err(TypesError::invalid_config(
381 "disk_snapshot_ref is not supported here: resolve the snapshot reference \
382 to a concrete image before converting to a sandbox spec",
383 )),
384 }
385 }
386}
387
388impl CloudCreateSandboxRequest {
389 pub const fn sandbox_spec(&self) -> &CloudSandboxSpec {
391 match self {
392 Self::Oci { sandbox, .. }
393 | Self::Bind { sandbox, .. }
394 | Self::DiskImage { sandbox, .. }
395 | Self::DiskSnapshot { sandbox, .. } => sandbox,
396 }
397 }
398
399 pub const fn sandbox_spec_mut(&mut self) -> &mut CloudSandboxSpec {
401 match self {
402 Self::Oci { sandbox, .. }
403 | Self::Bind { sandbox, .. }
404 | Self::DiskImage { sandbox, .. }
405 | Self::DiskSnapshot { sandbox, .. } => sandbox,
406 }
407 }
408
409 pub const fn disk_snapshot_ref(&self) -> Option<&CloudSnapshotLocation> {
411 match self {
412 Self::DiskSnapshot {
413 disk_snapshot_ref, ..
414 } => Some(disk_snapshot_ref),
415 _ => None,
416 }
417 }
418
419 pub fn oci_reference(&self) -> Option<&str> {
421 match self {
422 Self::Oci { reference, .. } => Some(reference),
423 _ => None,
424 }
425 }
426
427 pub const fn compute_resources(&self) -> CloudSandboxComputeResources {
429 match self {
430 Self::Oci { resources, .. } => CloudSandboxComputeResources {
431 vcpus: resources.vcpus,
432 memory_mib: resources.memory_mib,
433 },
434 Self::Bind { resources, .. }
435 | Self::DiskImage { resources, .. }
436 | Self::DiskSnapshot { resources, .. } => *resources,
437 }
438 }
439
440 pub const fn oci_disk_size_mib(&self) -> Option<Option<u32>> {
442 match self {
443 Self::Oci { resources, .. } => Some(resources.disk_size_mib),
444 _ => None,
445 }
446 }
447
448 pub fn set_oci_disk_size_mib(&mut self, disk_size_mib: u32) -> bool {
450 let Self::Oci { resources, .. } = self else {
451 return false;
452 };
453 resources.disk_size_mib = Some(disk_size_mib);
454 true
455 }
456}
457
458impl CloudSandboxSpec {
459 fn into_domain_spec(
460 self,
461 image: RootfsSource,
462 resources: CloudSandboxComputeResources,
463 patches: Vec<CloudPatch>,
464 pull_policy: CloudPullPolicy,
465 ) -> TypesResult<SandboxSpec> {
466 let resources = SandboxResources {
467 cpus: resources.vcpus,
468 memory_mib: resources.memory_mib,
469 max_cpus: resources.vcpus,
473 max_memory_mib: resources.memory_mib,
474 cpu_placement: CpuPlacement::Inherit,
478 placement_profile: None,
479 thp: TransparentHugePagePolicy::Madvise,
480 };
481
482 let network = NetworkSpec {
486 enabled: self.network.enabled,
487 interface: None,
488 ports: Vec::new(),
489 policy: self.network.policy,
490 dns: None,
491 tls: None,
492 strict: self.network.strict,
493 secrets: self.network.secrets.map(Into::into),
494 max_tcp_connections: self.network.max_tcp_connections,
495 max_udp_connections: self.network.max_udp_connections,
496 rate_limiter: None,
497 trust_host_cas: false,
498 outbound_proxy: None,
499 };
500 let runtime = SandboxRuntimeOptions {
501 workdir: self.runtime.workdir,
502 shell: self.runtime.shell,
503 scripts: self.runtime.scripts,
504 entrypoint: self.runtime.entrypoint,
505 cmd: self.runtime.cmd,
506 hostname: None,
507 user: self.runtime.user,
508 log_level: self.runtime.log_level,
509 metrics_sample_interval_ms: None,
510 disable_metrics_sample: false,
511 };
512
513 Ok(SandboxSpec {
514 name: self.name,
515 image,
516 resources,
517 runtime,
518 env: self.env,
519 labels: self.labels,
520 rlimits: self.rlimits.into_iter().map(Into::into).collect(),
521 mounts: self.mounts.into_iter().map(Into::into).collect(),
522 patches: patches.into_iter().map(Into::into).collect(),
523 network,
524 vsock: VsockSpec::default(),
525 init: self.init,
526 pull_policy: pull_policy.into(),
527 security_profile: self.security_profile,
528 deployment_profile: DeploymentProfile::default(),
529 lifecycle: self.lifecycle,
530 })
531 }
532}
533
534impl From<SandboxSpec> for CloudCreateSandboxRequest {
535 fn from(spec: SandboxSpec) -> Self {
536 let resources = CloudSandboxComputeResources {
537 vcpus: spec.resources.cpus,
538 memory_mib: spec.resources.memory_mib,
539 };
540 let patches = spec.patches.into_iter().map(Into::into).collect();
541 let pull_policy = spec.pull_policy.into();
542 let sandbox = CloudSandboxSpec {
543 name: spec.name,
544 runtime: CloudSandboxRuntimeOptions {
545 workdir: spec.runtime.workdir,
546 shell: spec.runtime.shell,
547 scripts: spec.runtime.scripts,
548 entrypoint: spec.runtime.entrypoint,
549 cmd: spec.runtime.cmd,
550 user: spec.runtime.user,
551 log_level: spec.runtime.log_level,
552 },
553 env: spec.env,
554 labels: spec.labels,
555 rlimits: spec.rlimits.into_iter().map(Into::into).collect(),
556 mounts: spec.mounts.into_iter().map(Into::into).collect(),
557 network: CloudNetworkSpec {
558 enabled: spec.network.enabled,
559 policy: spec.network.policy,
560 secrets: spec.network.secrets.map(Into::into),
561 strict: spec.network.strict,
562 max_tcp_connections: spec.network.max_tcp_connections,
563 max_udp_connections: spec.network.max_udp_connections,
564 },
565 init: spec.init,
566 security_profile: spec.security_profile,
567 lifecycle: spec.lifecycle,
568 };
569
570 match spec.image {
571 RootfsSource::Oci(oci) => Self::Oci {
572 sandbox,
573 reference: oci.reference,
574 resources: CloudSandboxResources {
575 vcpus: resources.vcpus,
576 memory_mib: resources.memory_mib,
577 disk_size_mib: match oci.root_disk {
578 Some(RootDisk::Managed { size_mib }) => size_mib,
579 _ => None,
580 },
581 },
582 patches,
583 pull_policy,
584 },
585 RootfsSource::Bind { path, .. } => Self::Bind {
586 sandbox,
587 path,
588 resources,
589 patches,
590 },
591 RootfsSource::DiskImage {
592 path,
593 format,
594 fstype,
595 } => Self::DiskImage {
596 sandbox,
597 path,
598 format: format.into(),
599 fstype,
600 resources,
601 patches,
602 },
603 }
604 }
605}
606
607impl Default for CloudSandboxResources {
608 fn default() -> Self {
609 let resources = SandboxResources::default();
610 Self {
611 vcpus: resources.cpus,
612 memory_mib: resources.memory_mib,
613 disk_size_mib: None,
614 }
615 }
616}
617
618impl Default for CloudSandboxComputeResources {
619 fn default() -> Self {
620 let resources = SandboxResources::default();
621 Self {
622 vcpus: resources.cpus,
623 memory_mib: resources.memory_mib,
624 }
625 }
626}
627
628impl From<CloudSandboxResources> for CloudSandboxComputeResources {
629 fn from(resources: CloudSandboxResources) -> Self {
630 Self {
631 vcpus: resources.vcpus,
632 memory_mib: resources.memory_mib,
633 }
634 }
635}
636
637impl Default for CloudCreateSandboxRequest {
638 fn default() -> Self {
639 Self::Oci {
640 sandbox: CloudSandboxSpec::default(),
641 reference: String::new(),
642 resources: CloudSandboxResources::default(),
643 patches: Vec::new(),
644 pull_policy: CloudPullPolicy::default(),
645 }
646 }
647}
648
649impl CloudRootfsSource {
650 pub fn oci(reference: impl Into<String>) -> Self {
652 Self::Oci {
653 reference: reference.into(),
654 }
655 }
656
657 pub fn oci_reference(&self) -> Option<&str> {
659 match self {
660 Self::Oci { reference } => Some(reference),
661 _ => None,
662 }
663 }
664}
665
666impl Default for CloudRootfsSource {
667 fn default() -> Self {
668 Self::oci(String::new())
669 }
670}
671
672#[cfg(test)]
673mod tests;