use std::collections::BTreeMap;
use std::path::PathBuf;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::domain::{
CpuPlacement, DeploymentProfile, EnvVar, HandoffInit, NetworkSpec, OciRootfsSource, RootDisk,
RootfsSource, SandboxPolicy, SandboxResources, SandboxRuntimeOptions, SandboxSpec,
SecurityProfile, TransparentHugePagePolicy, VsockSpec,
};
use crate::{TypesError, TypesResult};
mod compat;
mod secrets;
mod snapshots;
mod specs;
pub use secrets::{
CloudHostPattern, CloudSecretEntry, CloudSecretSource, CloudSecretsConfig, CloudViolationAction,
};
pub use snapshots::{
CloudCreateSnapshotRequest, CloudSnapshot, CloudSnapshotDetails, CloudSnapshotKind,
CloudSnapshotLocation, CloudSnapshotOperation, CloudSnapshotOperationStatus, CloudSnapshotSpec,
};
pub use specs::{
CloudDiskImageFormat, CloudNetworkSpec, CloudPatch, CloudPullPolicy, CloudRlimit,
CloudRlimitResource, CloudRootfsSource, CloudSandboxRuntimeOptions, CloudVolumeMount,
};
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(tag = "source", rename_all = "snake_case")]
pub enum CloudCreateSandboxRequest {
Oci {
#[serde(flatten)]
sandbox: CloudSandboxSpec,
reference: String,
#[serde(default)]
resources: CloudSandboxResources,
#[serde(default)]
patches: Vec<CloudPatch>,
#[serde(default)]
pull_policy: CloudPullPolicy,
},
Bind {
#[serde(flatten)]
sandbox: CloudSandboxSpec,
#[cfg_attr(feature = "ts", ts(type = "string"))]
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
path: PathBuf,
#[serde(default)]
resources: CloudSandboxComputeResources,
#[serde(default)]
patches: Vec<CloudPatch>,
},
DiskImage {
#[serde(flatten)]
sandbox: CloudSandboxSpec,
#[cfg_attr(feature = "ts", ts(type = "string"))]
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
path: PathBuf,
format: CloudDiskImageFormat,
fstype: Option<String>,
#[serde(default)]
resources: CloudSandboxComputeResources,
#[serde(default)]
patches: Vec<CloudPatch>,
},
DiskSnapshot {
#[serde(flatten)]
sandbox: CloudSandboxSpec,
disk_snapshot_ref: CloudSnapshotLocation,
#[serde(default)]
resources: CloudSandboxComputeResources,
#[serde(default)]
pull_policy: CloudPullPolicy,
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(default)]
pub struct CloudSandboxSpec {
#[cfg_attr(feature = "utoipa", schema(required = true))]
pub name: String,
pub runtime: CloudSandboxRuntimeOptions,
pub env: Vec<EnvVar>,
pub labels: BTreeMap<String, String>,
pub rlimits: Vec<CloudRlimit>,
pub mounts: Vec<CloudVolumeMount>,
pub network: CloudNetworkSpec,
pub init: Option<HandoffInit>,
pub security_profile: SecurityProfile,
pub lifecycle: SandboxPolicy,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(default)]
pub struct CloudSandboxComputeResources {
pub vcpus: u8,
pub memory_mib: u32,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(default)]
pub struct CloudSandboxResources {
pub vcpus: u8,
pub memory_mib: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disk_size_mib: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct CloudCreateSandboxResponse {
pub id: String,
pub org_id: String,
pub name: String,
pub slug: String,
pub status: CloudSandboxStatus,
#[serde(default)]
pub status_reason: Option<CloudSandboxStatusReason>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "ts", ts(type = "unknown | null | undefined"))]
pub spec: Option<serde_json::Value>,
pub ephemeral: bool,
#[cfg_attr(feature = "ts", ts(type = "string"))]
pub created_at: DateTime<Utc>,
#[serde(default)]
#[cfg_attr(feature = "ts", ts(type = "string | null"))]
pub started_at: Option<DateTime<Utc>>,
#[serde(default)]
#[cfg_attr(feature = "ts", ts(type = "string | null"))]
pub stopped_at: Option<DateTime<Utc>>,
#[serde(default)]
pub last_failure_message: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum CloudSandboxStatus {
Created,
Starting,
Running,
Stopping,
Stopped,
Failed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum CloudSandboxStatusReason {
Scheduling,
InsufficientCapacity,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct CloudPaginated<T> {
pub data: Vec<T>,
#[serde(default)]
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct CloudMessageResponse {
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct CloudErrorBody {
#[serde(default)]
pub code: Option<String>,
#[serde(default)]
pub message: Option<String>,
#[serde(default)]
pub error: Option<CloudErrorDetails>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct CloudErrorDetails {
#[serde(default)]
pub code: Option<String>,
#[serde(default)]
pub message: Option<String>,
}
impl TryFrom<CloudCreateSandboxRequest> for SandboxSpec {
type Error = TypesError;
fn try_from(req: CloudCreateSandboxRequest) -> TypesResult<Self> {
match req {
CloudCreateSandboxRequest::Oci {
sandbox,
reference,
resources,
patches,
pull_policy,
} => sandbox.into_domain_spec(
RootfsSource::Oci(OciRootfsSource {
reference,
root_disk: resources.disk_size_mib.map(RootDisk::managed),
}),
resources.into(),
patches,
pull_policy,
),
CloudCreateSandboxRequest::Bind {
sandbox,
path,
resources,
patches,
} => sandbox.into_domain_spec(
RootfsSource::Bind {
path,
follow_root_symlinks: false,
},
resources,
patches,
CloudPullPolicy::default(),
),
CloudCreateSandboxRequest::DiskImage {
sandbox,
path,
format,
fstype,
resources,
patches,
} => sandbox.into_domain_spec(
RootfsSource::DiskImage {
path,
format: format.into(),
fstype,
},
resources,
patches,
CloudPullPolicy::default(),
),
CloudCreateSandboxRequest::DiskSnapshot { .. } => Err(TypesError::invalid_config(
"disk_snapshot_ref is not supported here: resolve the snapshot reference \
to a concrete image before converting to a sandbox spec",
)),
}
}
}
impl CloudCreateSandboxRequest {
pub const fn sandbox_spec(&self) -> &CloudSandboxSpec {
match self {
Self::Oci { sandbox, .. }
| Self::Bind { sandbox, .. }
| Self::DiskImage { sandbox, .. }
| Self::DiskSnapshot { sandbox, .. } => sandbox,
}
}
pub const fn sandbox_spec_mut(&mut self) -> &mut CloudSandboxSpec {
match self {
Self::Oci { sandbox, .. }
| Self::Bind { sandbox, .. }
| Self::DiskImage { sandbox, .. }
| Self::DiskSnapshot { sandbox, .. } => sandbox,
}
}
pub const fn disk_snapshot_ref(&self) -> Option<&CloudSnapshotLocation> {
match self {
Self::DiskSnapshot {
disk_snapshot_ref, ..
} => Some(disk_snapshot_ref),
_ => None,
}
}
pub fn oci_reference(&self) -> Option<&str> {
match self {
Self::Oci { reference, .. } => Some(reference),
_ => None,
}
}
pub const fn compute_resources(&self) -> CloudSandboxComputeResources {
match self {
Self::Oci { resources, .. } => CloudSandboxComputeResources {
vcpus: resources.vcpus,
memory_mib: resources.memory_mib,
},
Self::Bind { resources, .. }
| Self::DiskImage { resources, .. }
| Self::DiskSnapshot { resources, .. } => *resources,
}
}
pub const fn oci_disk_size_mib(&self) -> Option<Option<u32>> {
match self {
Self::Oci { resources, .. } => Some(resources.disk_size_mib),
_ => None,
}
}
pub fn set_oci_disk_size_mib(&mut self, disk_size_mib: u32) -> bool {
let Self::Oci { resources, .. } = self else {
return false;
};
resources.disk_size_mib = Some(disk_size_mib);
true
}
}
impl CloudSandboxSpec {
fn into_domain_spec(
self,
image: RootfsSource,
resources: CloudSandboxComputeResources,
patches: Vec<CloudPatch>,
pull_policy: CloudPullPolicy,
) -> TypesResult<SandboxSpec> {
let resources = SandboxResources {
cpus: resources.vcpus,
memory_mib: resources.memory_mib,
max_cpus: resources.vcpus,
max_memory_mib: resources.memory_mib,
cpu_placement: CpuPlacement::Inherit,
placement_profile: None,
thp: TransparentHugePagePolicy::Madvise,
};
let network = NetworkSpec {
enabled: self.network.enabled,
interface: None,
ports: Vec::new(),
policy: self.network.policy,
dns: None,
tls: None,
strict: self.network.strict,
secrets: self.network.secrets.map(Into::into),
max_tcp_connections: self.network.max_tcp_connections,
max_udp_connections: self.network.max_udp_connections,
rate_limiter: None,
trust_host_cas: false,
outbound_proxy: None,
};
let runtime = SandboxRuntimeOptions {
workdir: self.runtime.workdir,
shell: self.runtime.shell,
scripts: self.runtime.scripts,
entrypoint: self.runtime.entrypoint,
cmd: self.runtime.cmd,
hostname: None,
user: self.runtime.user,
log_level: self.runtime.log_level,
metrics_sample_interval_ms: None,
disable_metrics_sample: false,
};
Ok(SandboxSpec {
name: self.name,
image,
resources,
runtime,
env: self.env,
labels: self.labels,
rlimits: self.rlimits.into_iter().map(Into::into).collect(),
mounts: self.mounts.into_iter().map(Into::into).collect(),
patches: patches.into_iter().map(Into::into).collect(),
network,
vsock: VsockSpec::default(),
init: self.init,
pull_policy: pull_policy.into(),
security_profile: self.security_profile,
deployment_profile: DeploymentProfile::default(),
lifecycle: self.lifecycle,
})
}
}
impl From<SandboxSpec> for CloudCreateSandboxRequest {
fn from(spec: SandboxSpec) -> Self {
let resources = CloudSandboxComputeResources {
vcpus: spec.resources.cpus,
memory_mib: spec.resources.memory_mib,
};
let patches = spec.patches.into_iter().map(Into::into).collect();
let pull_policy = spec.pull_policy.into();
let sandbox = CloudSandboxSpec {
name: spec.name,
runtime: CloudSandboxRuntimeOptions {
workdir: spec.runtime.workdir,
shell: spec.runtime.shell,
scripts: spec.runtime.scripts,
entrypoint: spec.runtime.entrypoint,
cmd: spec.runtime.cmd,
user: spec.runtime.user,
log_level: spec.runtime.log_level,
},
env: spec.env,
labels: spec.labels,
rlimits: spec.rlimits.into_iter().map(Into::into).collect(),
mounts: spec.mounts.into_iter().map(Into::into).collect(),
network: CloudNetworkSpec {
enabled: spec.network.enabled,
policy: spec.network.policy,
secrets: spec.network.secrets.map(Into::into),
strict: spec.network.strict,
max_tcp_connections: spec.network.max_tcp_connections,
max_udp_connections: spec.network.max_udp_connections,
},
init: spec.init,
security_profile: spec.security_profile,
lifecycle: spec.lifecycle,
};
match spec.image {
RootfsSource::Oci(oci) => Self::Oci {
sandbox,
reference: oci.reference,
resources: CloudSandboxResources {
vcpus: resources.vcpus,
memory_mib: resources.memory_mib,
disk_size_mib: match oci.root_disk {
Some(RootDisk::Managed { size_mib }) => size_mib,
_ => None,
},
},
patches,
pull_policy,
},
RootfsSource::Bind { path, .. } => Self::Bind {
sandbox,
path,
resources,
patches,
},
RootfsSource::DiskImage {
path,
format,
fstype,
} => Self::DiskImage {
sandbox,
path,
format: format.into(),
fstype,
resources,
patches,
},
}
}
}
impl Default for CloudSandboxResources {
fn default() -> Self {
let resources = SandboxResources::default();
Self {
vcpus: resources.cpus,
memory_mib: resources.memory_mib,
disk_size_mib: None,
}
}
}
impl Default for CloudSandboxComputeResources {
fn default() -> Self {
let resources = SandboxResources::default();
Self {
vcpus: resources.cpus,
memory_mib: resources.memory_mib,
}
}
}
impl From<CloudSandboxResources> for CloudSandboxComputeResources {
fn from(resources: CloudSandboxResources) -> Self {
Self {
vcpus: resources.vcpus,
memory_mib: resources.memory_mib,
}
}
}
impl Default for CloudCreateSandboxRequest {
fn default() -> Self {
Self::Oci {
sandbox: CloudSandboxSpec::default(),
reference: String::new(),
resources: CloudSandboxResources::default(),
patches: Vec::new(),
pull_policy: CloudPullPolicy::default(),
}
}
}
impl CloudRootfsSource {
pub fn oci(reference: impl Into<String>) -> Self {
Self::Oci {
reference: reference.into(),
}
}
pub fn oci_reference(&self) -> Option<&str> {
match self {
Self::Oci { reference } => Some(reference),
_ => None,
}
}
}
impl Default for CloudRootfsSource {
fn default() -> Self {
Self::oci(String::new())
}
}
#[cfg(test)]
mod tests;