Skip to main content

sail/sailbox/
types.rs

1//! Typed domain models for the sailbox lifecycle API.
2//!
3//! The core owns the wire schema: it deserializes API responses into these
4//! structs (applying the wire-schema field defaults) and serializes them back
5//! out for bindings. A binding maps a struct onto its own public type by field
6//! name without re-parsing the wire shape.
7
8use serde::{Deserialize, Serialize};
9
10/// A running-sailbox handle: the fields needed to exec/file/listener against a
11/// live box. Returned by create/resume/from_checkpoint.
12#[derive(Debug, Clone, Serialize)]
13pub struct SailboxHandle {
14    /// The sailbox's stable identifier.
15    pub sailbox_id: String,
16    /// The caller-supplied sailbox name.
17    pub name: String,
18    /// The sailbox's lifecycle status (for example `running`).
19    pub status: String,
20    /// Network address of the worker hosting the sailbox.
21    pub worker_address: String,
22    /// Endpoint used to open exec/file/listener streams to the box.
23    pub exec_endpoint: String,
24}
25
26/// Read-only snapshot from get/list. Deliberately omits worker_address /
27/// exec_endpoint (the list/get endpoints do not return them).
28#[derive(Debug, Clone, Serialize)]
29pub struct SailboxInfo {
30    /// The sailbox's stable identifier.
31    pub sailbox_id: String,
32    /// Identifier of the owning app.
33    pub app_id: String,
34    /// Name of the owning app.
35    pub app_name: String,
36    /// Identifier of the image the sailbox was created from.
37    pub image_id: String,
38    /// The caller-supplied sailbox name.
39    pub name: String,
40    /// The sailbox's lifecycle status (for example `running`).
41    pub status: String,
42    /// Configured memory size, in mebibytes.
43    pub memory_mib: i64,
44    /// Configured number of virtual CPUs.
45    pub vcpu_count: i64,
46    /// Configured state-disk size, in gibibytes.
47    pub state_disk_size_gib: i64,
48    /// Requested CPU allocation, in vCPUs.
49    pub cpu_requested_vcpu: i64,
50    /// Current CPU usage, in vCPUs.
51    pub cpu_used_vcpu: f64,
52    /// Requested memory allocation, in bytes.
53    pub memory_requested_bytes: i64,
54    /// Current memory usage, in bytes.
55    pub memory_used_bytes: i64,
56    /// Requested disk allocation, in bytes.
57    pub disk_requested_bytes: i64,
58    /// Current disk usage, in bytes.
59    pub disk_used_bytes: i64,
60    /// CPU architecture of the sailbox (for example `x86_64` or `arm64`).
61    pub architecture: String,
62    /// Guest schema version the box is running, when reported by the backend.
63    pub guest_schema_version: Option<i64>,
64    /// Human-readable error detail, present when the box is in an error state.
65    pub error_message: Option<String>,
66    /// Monotonic checkpoint generation counter for the sailbox.
67    pub checkpoint_generation: i64,
68    /// RFC 3339 timestamp of when the box last started, if it has started.
69    pub started_at: Option<String>,
70    /// RFC 3339 timestamp of the most recent checkpoint, if any.
71    pub last_checkpointed_at: Option<String>,
72    /// RFC 3339 timestamp of when the sailbox was created.
73    pub created_at: String,
74    /// RFC 3339 timestamp of the sailbox's last update.
75    pub updated_at: String,
76}
77
78/// Wire form: some backends omit the requested-resource fields, so they are
79/// optional and fall back to vcpu_count/memory_mib/state_disk_size_gib in [`From`].
80#[derive(Deserialize)]
81pub(crate) struct SailboxInfoWire {
82    sailbox_id: String,
83    app_id: String,
84    app_name: String,
85    #[serde(default)]
86    image_id: String,
87    name: String,
88    status: String,
89    memory_mib: i64,
90    vcpu_count: i64,
91    state_disk_size_gib: i64,
92    cpu_requested_vcpu: Option<i64>,
93    cpu_used_vcpu: Option<f64>,
94    memory_requested_bytes: Option<i64>,
95    memory_used_bytes: Option<i64>,
96    disk_requested_bytes: Option<i64>,
97    disk_used_bytes: Option<i64>,
98    architecture: String,
99    guest_schema_version: Option<i64>,
100    error_message: Option<String>,
101    checkpoint_generation: Option<i64>,
102    started_at: Option<String>,
103    last_checkpointed_at: Option<String>,
104    created_at: String,
105    updated_at: String,
106}
107
108impl From<SailboxInfoWire> for SailboxInfo {
109    fn from(w: SailboxInfoWire) -> SailboxInfo {
110        let memory_bytes = w.memory_mib.saturating_mul(1024 * 1024);
111        let disk_bytes = w.state_disk_size_gib.saturating_mul(1024 * 1024 * 1024);
112        SailboxInfo {
113            sailbox_id: w.sailbox_id,
114            app_id: w.app_id,
115            app_name: w.app_name,
116            image_id: w.image_id,
117            name: w.name,
118            status: w.status,
119            memory_mib: w.memory_mib,
120            vcpu_count: w.vcpu_count,
121            state_disk_size_gib: w.state_disk_size_gib,
122            cpu_requested_vcpu: w.cpu_requested_vcpu.unwrap_or(w.vcpu_count),
123            cpu_used_vcpu: w.cpu_used_vcpu.unwrap_or(0.0),
124            memory_requested_bytes: w.memory_requested_bytes.unwrap_or(memory_bytes),
125            memory_used_bytes: w.memory_used_bytes.unwrap_or(0),
126            disk_requested_bytes: w.disk_requested_bytes.unwrap_or(disk_bytes),
127            disk_used_bytes: w.disk_used_bytes.unwrap_or(0),
128            architecture: w.architecture,
129            guest_schema_version: w.guest_schema_version,
130            error_message: w.error_message,
131            checkpoint_generation: w.checkpoint_generation.unwrap_or(0),
132            started_at: w.started_at,
133            last_checkpointed_at: w.last_checkpointed_at,
134            created_at: w.created_at,
135            updated_at: w.updated_at,
136        }
137    }
138}
139
140/// One page of list results plus the pagination envelope.
141#[derive(Debug, Clone, Serialize)]
142pub struct SailboxPage {
143    /// The sailboxes in this page.
144    pub items: Vec<SailboxInfo>,
145    /// Maximum number of items requested for this page.
146    pub limit: i64,
147    /// Zero-based offset of the first item in this page.
148    pub offset: i64,
149    /// Total number of sailboxes matching the query across all pages.
150    pub total: i64,
151    /// True when further pages exist beyond this one.
152    pub has_more: bool,
153}
154
155/// A durable checkpoint handle. `status` echoes the source sailbox's lifecycle
156/// status after checkpointing (a running box is snapshotted; a paused/sleeping
157/// one returns its existing checkpoint), so the binding can sync its handle.
158#[derive(Debug, Clone, Serialize)]
159pub struct SailboxCheckpoint {
160    /// Stable identifier of the checkpoint.
161    pub checkpoint_id: String,
162    /// Identifier of the sailbox the checkpoint was taken from.
163    pub sailbox_id: String,
164    /// Checkpoint generation counter captured by this checkpoint.
165    pub checkpoint_generation: i64,
166    /// Source sailbox's lifecycle status after checkpointing.
167    pub status: String,
168}
169
170/// Filters for list/list_page.
171#[derive(Debug, Clone)]
172pub struct ListQuery {
173    /// Restrict results to sailboxes owned by this app (id or name).
174    pub app: Option<String>,
175    /// Restrict results to sailboxes in this lifecycle status.
176    pub status: Option<String>,
177    /// Free-text search filter applied by the backend.
178    pub search: Option<String>,
179    /// Exclude sailboxes whose guest schema version exceeds this value.
180    pub max_guest_schema_version: Option<i64>,
181    /// Maximum number of items to return.
182    pub limit: i64,
183    /// Zero-based offset of the first item to return.
184    pub offset: i64,
185}
186
187/// Default page size for listing, matching the sailbox API's own default. The
188/// API rejects `limit=0`, so the derived all-zero default cannot be used.
189pub const DEFAULT_LIST_LIMIT: i64 = 50;
190
191/// Resource-limit bounds enforced at create time. These mirror the scheduler's
192/// authoritative limits so every binding (CLI, Python, future SDKs) fails fast
193/// with the same error instead of round-tripping to a backend rejection.
194pub const MAX_SAILBOX_VCPUS: i64 = 4;
195/// Minimum requested memory in MiB (0 is allowed and means "use the default").
196pub const MIN_SAILBOX_MEMORY_MIB: i64 = 1024;
197/// Maximum requested memory in MiB.
198pub const MAX_SAILBOX_MEMORY_MIB: i64 = 8192;
199/// Maximum requested state-disk size in GiB.
200pub const MAX_SAILBOX_DISK_GIB: i64 = 32;
201
202impl Default for ListQuery {
203    fn default() -> ListQuery {
204        ListQuery {
205            app: None,
206            status: None,
207            search: None,
208            max_guest_schema_version: None,
209            limit: DEFAULT_LIST_LIMIT,
210            offset: 0,
211        }
212    }
213}
214
215/// One guest ingress port to reserve at create time.
216#[derive(Debug, Clone, Serialize)]
217pub struct IngressPort {
218    /// Port inside the guest to expose for ingress.
219    pub guest_port: u32,
220    /// Transport protocol for the port (for example `tcp`).
221    pub protocol: String,
222    /// Source addresses allowed to reach the port; empty means no restriction.
223    #[serde(skip_serializing_if = "Vec::is_empty")]
224    pub allowlist: Vec<String>,
225}
226
227/// One NFS volume mount.
228#[derive(Debug, Clone, Serialize)]
229pub struct VolumeMount {
230    /// Identifier of the NFS volume to mount.
231    pub volume_id: String,
232    /// Absolute path inside the guest where the volume is mounted.
233    pub mount_path: String,
234}
235
236/// A managed NFS volume as returned by the sailbox-volume API. (The local
237/// mount path is a binding-side concern and not part of this wire type.)
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct NfsVolume {
240    /// Stable identifier of the volume.
241    pub volume_id: String,
242    /// Caller-supplied volume name.
243    #[serde(default)]
244    pub name: String,
245    /// Storage backend serving the volume.
246    #[serde(default)]
247    pub backend: String,
248    /// Lifecycle status of the volume.
249    #[serde(default)]
250    pub status: String,
251    /// RFC 3339 timestamp of when the volume was created, if reported.
252    #[serde(default)]
253    pub created_at: Option<String>,
254    /// RFC 3339 timestamp of the volume's last update, if reported.
255    #[serde(default)]
256    pub updated_at: Option<String>,
257}
258
259/// Inputs to create a sailbox. The image is a binding-built spec passed through
260/// opaquely (the image DSL lives in the language wrapper), while the lifecycle
261/// envelope is owned by the core.
262#[derive(Debug, Clone)]
263pub struct CreateSailboxRequest {
264    /// Identifier of the app that will own the sailbox.
265    pub app_id: String,
266    /// Caller-supplied name for the sailbox.
267    pub name: String,
268    /// Guest ingress ports to reserve at create time.
269    pub ingress_ports: Vec<IngressPort>,
270    /// NFS volumes to mount into the guest.
271    pub volume_mounts: Vec<VolumeMount>,
272    /// The image spec as a JSON object built by the binding's image DSL.
273    pub image: serde_json::Value,
274    /// Requested CPU allocation in vCPUs; defaults server-side when absent.
275    pub cpu: Option<i64>,
276    /// Requested memory size in mebibytes; defaults server-side when absent.
277    pub memory_mib: Option<i64>,
278    /// Requested state-disk size in gibibytes; defaults server-side when absent.
279    pub state_disk_size_gib: Option<i64>,
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn list_query_default_uses_a_valid_nonzero_limit() {
288        // The sailbox API rejects limit=0, so the default must be the API's own
289        // page size, not the derived zero.
290        let query = ListQuery::default();
291        assert_eq!(query.limit, DEFAULT_LIST_LIMIT);
292        assert!(query.limit > 0);
293        assert_eq!(query.offset, 0);
294    }
295}