Skip to main content

sail/sailbox/
types.rs

1//! Typed domain models for the sailbox API: lifecycle, listing, ingress
2//! listeners, and volumes.
3//!
4//! The core owns the wire schema: it deserializes API responses into these
5//! structs (applying the wire-schema field defaults) and serializes them back
6//! out for bindings. A binding maps a struct onto its own public type by field
7//! name without re-parsing the wire shape.
8
9use serde::{Deserialize, Serialize};
10use time::OffsetDateTime;
11
12/// Lifecycle status of a sailbox. An unrecognized server value is preserved in
13/// `Other` so a status a newer backend introduces never fails parsing. The
14/// default is `Other("")`: the status of a handle no server response has
15/// filled in yet.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum SailboxStatus {
18    /// Running and serving.
19    Running,
20    /// Paused in memory.
21    Paused,
22    /// Sleeping (checkpointed to disk).
23    Sleeping,
24    /// In a failed state.
25    Failed,
26    /// Terminated.
27    Terminated,
28    /// A status this SDK version does not recognize, kept verbatim.
29    Other(String),
30}
31
32impl Default for SailboxStatus {
33    fn default() -> Self {
34        SailboxStatus::Other(String::new())
35    }
36}
37
38impl SailboxStatus {
39    /// The wire string for this status.
40    pub fn as_str(&self) -> &str {
41        match self {
42            SailboxStatus::Running => "running",
43            SailboxStatus::Paused => "paused",
44            SailboxStatus::Sleeping => "sleeping",
45            SailboxStatus::Failed => "failed",
46            SailboxStatus::Terminated => "terminated",
47            SailboxStatus::Other(s) => s,
48        }
49    }
50}
51
52impl From<&str> for SailboxStatus {
53    fn from(s: &str) -> SailboxStatus {
54        match s {
55            "running" => SailboxStatus::Running,
56            "paused" => SailboxStatus::Paused,
57            "sleeping" => SailboxStatus::Sleeping,
58            "failed" => SailboxStatus::Failed,
59            "terminated" => SailboxStatus::Terminated,
60            other => SailboxStatus::Other(other.to_string()),
61        }
62    }
63}
64
65impl std::fmt::Display for SailboxStatus {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        f.write_str(self.as_str())
68    }
69}
70
71impl Serialize for SailboxStatus {
72    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
73        serializer.serialize_str(self.as_str())
74    }
75}
76
77impl<'de> Deserialize<'de> for SailboxStatus {
78    fn deserialize<D: serde::Deserializer<'de>>(
79        deserializer: D,
80    ) -> Result<SailboxStatus, D::Error> {
81        Ok(SailboxStatus::from(
82            String::deserialize(deserializer)?.as_str(),
83        ))
84    }
85}
86
87/// A lifecycle status a client can filter by in [`ListSailboxesQuery`]. A closed set: a
88/// client only filters by statuses the SDK knows. Server responses use the open
89/// [`SailboxStatus`], which tolerates a status a newer backend introduces.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum SailboxStatusFilter {
92    /// Running and serving.
93    Running,
94    /// Paused in memory.
95    Paused,
96    /// Sleeping (checkpointed to disk).
97    Sleeping,
98    /// In a failed state.
99    Failed,
100    /// Terminated.
101    Terminated,
102}
103
104/// Server-side ordering for list results.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
106pub enum SailboxListOrder {
107    /// Activity recency: active/restorable boxes first, then updated_at.
108    #[default]
109    Activity,
110    /// Newest-created boxes first.
111    CreatedAtDesc,
112}
113
114impl SailboxListOrder {
115    /// The wire string for this order.
116    pub fn as_str(&self) -> &'static str {
117        match self {
118            SailboxListOrder::Activity => "activity",
119            SailboxListOrder::CreatedAtDesc => "created_at_desc",
120        }
121    }
122
123    /// Parse a wire string.
124    pub fn parse(raw: &str) -> Option<SailboxListOrder> {
125        match raw.trim() {
126            "activity" => Some(SailboxListOrder::Activity),
127            "created_at_desc" => Some(SailboxListOrder::CreatedAtDesc),
128            _ => None,
129        }
130    }
131}
132
133impl SailboxStatusFilter {
134    /// The wire string for this status.
135    pub fn as_str(&self) -> &'static str {
136        match self {
137            SailboxStatusFilter::Running => "running",
138            SailboxStatusFilter::Paused => "paused",
139            SailboxStatusFilter::Sleeping => "sleeping",
140            SailboxStatusFilter::Failed => "failed",
141            SailboxStatusFilter::Terminated => "terminated",
142        }
143    }
144
145    /// Parse a wire string, returning `None` for a status the SDK does not know.
146    pub fn parse(s: &str) -> Option<SailboxStatusFilter> {
147        match s {
148            "running" => Some(SailboxStatusFilter::Running),
149            "paused" => Some(SailboxStatusFilter::Paused),
150            "sleeping" => Some(SailboxStatusFilter::Sleeping),
151            "failed" => Some(SailboxStatusFilter::Failed),
152            "terminated" => Some(SailboxStatusFilter::Terminated),
153            _ => None,
154        }
155    }
156}
157
158/// Wire protocol for an ingress port or listener. An unrecognized value is
159/// preserved in `Other` so a protocol a newer backend introduces never fails.
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub enum ListenerProtocol {
162    /// Raw TCP.
163    Tcp,
164    /// HTTP.
165    Http,
166    /// A protocol this SDK version does not recognize, kept verbatim.
167    Other(String),
168}
169
170impl ListenerProtocol {
171    /// The wire string for this protocol.
172    pub fn as_str(&self) -> &str {
173        match self {
174            ListenerProtocol::Tcp => "tcp",
175            ListenerProtocol::Http => "http",
176            ListenerProtocol::Other(s) => s,
177        }
178    }
179}
180
181impl From<&str> for ListenerProtocol {
182    fn from(s: &str) -> ListenerProtocol {
183        match s {
184            "tcp" => ListenerProtocol::Tcp,
185            "http" => ListenerProtocol::Http,
186            other => ListenerProtocol::Other(other.to_string()),
187        }
188    }
189}
190
191impl std::fmt::Display for ListenerProtocol {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        f.write_str(self.as_str())
194    }
195}
196
197impl Serialize for ListenerProtocol {
198    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
199        serializer.serialize_str(self.as_str())
200    }
201}
202
203impl<'de> Deserialize<'de> for ListenerProtocol {
204    fn deserialize<D: serde::Deserializer<'de>>(
205        deserializer: D,
206    ) -> Result<ListenerProtocol, D::Error> {
207        Ok(ListenerProtocol::from(
208            String::deserialize(deserializer)?.as_str(),
209        ))
210    }
211}
212
213/// Status of a listener's ingress route. An unrecognized value is preserved in
214/// `Other` so a status a newer backend introduces never fails to parse.
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub enum ListenerRouteStatus {
217    /// The route state is not yet known.
218    Unspecified,
219    /// The route is being set up.
220    Pending,
221    /// The route is active and ready to carry traffic.
222    Active,
223    /// The route is being restored after a checkpoint or migration.
224    Restoring,
225    /// The route is temporarily unavailable.
226    Unavailable,
227    /// A status this SDK version does not recognize, kept verbatim.
228    Other(String),
229}
230
231impl ListenerRouteStatus {
232    /// The user-facing string for this status. The backend's proto spelling
233    /// (`LISTENER_ROUTE_STATUS_*`) is normalized here, once, so no wrapper
234    /// ever sees it.
235    pub fn as_str(&self) -> &str {
236        match self {
237            ListenerRouteStatus::Unspecified => "unspecified",
238            ListenerRouteStatus::Pending => "pending",
239            ListenerRouteStatus::Active => "active",
240            ListenerRouteStatus::Restoring => "restoring",
241            ListenerRouteStatus::Unavailable => "unavailable",
242            ListenerRouteStatus::Other(s) => s,
243        }
244    }
245}
246
247impl From<&str> for ListenerRouteStatus {
248    /// Parse either the backend's proto spelling or the friendly form (so a
249    /// serialized [`Listener`](crate::Listener) round-trips).
250    fn from(s: &str) -> ListenerRouteStatus {
251        match s {
252            "LISTENER_ROUTE_STATUS_UNSPECIFIED" | "unspecified" => ListenerRouteStatus::Unspecified,
253            "LISTENER_ROUTE_STATUS_PENDING" | "pending" => ListenerRouteStatus::Pending,
254            "LISTENER_ROUTE_STATUS_ACTIVE" | "active" => ListenerRouteStatus::Active,
255            "LISTENER_ROUTE_STATUS_RESTORING" | "restoring" => ListenerRouteStatus::Restoring,
256            "LISTENER_ROUTE_STATUS_UNAVAILABLE" | "unavailable" => ListenerRouteStatus::Unavailable,
257            other => ListenerRouteStatus::Other(other.to_string()),
258        }
259    }
260}
261
262impl std::fmt::Display for ListenerRouteStatus {
263    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264        f.write_str(self.as_str())
265    }
266}
267
268impl Serialize for ListenerRouteStatus {
269    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
270        serializer.serialize_str(self.as_str())
271    }
272}
273
274impl<'de> Deserialize<'de> for ListenerRouteStatus {
275    fn deserialize<D: serde::Deserializer<'de>>(
276        deserializer: D,
277    ) -> Result<ListenerRouteStatus, D::Error> {
278        Ok(ListenerRouteStatus::from(
279            String::deserialize(deserializer)?.as_str(),
280        ))
281    }
282}
283
284/// Transport protocol a client requests when exposing an ingress port. A closed
285/// set: a client can only request a protocol the SDK supports. Server responses
286/// use the open [`ListenerProtocol`], which tolerates a protocol a newer backend adds.
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288pub enum IngressProtocol {
289    /// Raw TCP.
290    Tcp,
291    /// HTTP.
292    Http,
293}
294
295impl IngressProtocol {
296    /// The wire string for this protocol.
297    pub fn as_str(&self) -> &'static str {
298        match self {
299            IngressProtocol::Tcp => "tcp",
300            IngressProtocol::Http => "http",
301        }
302    }
303
304    /// Parse a wire string, returning `None` for a protocol the SDK does not
305    /// support.
306    pub fn parse(s: &str) -> Option<IngressProtocol> {
307        match s {
308            "tcp" => Some(IngressProtocol::Tcp),
309            "http" => Some(IngressProtocol::Http),
310            _ => None,
311        }
312    }
313}
314
315impl std::fmt::Display for IngressProtocol {
316    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317        f.write_str(self.as_str())
318    }
319}
320
321impl Serialize for IngressProtocol {
322    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
323        serializer.serialize_str(self.as_str())
324    }
325}
326
327/// Options for [`Sailbox::checkpoint`](crate::Sailbox::checkpoint).
328#[derive(Debug, Clone, Default)]
329pub struct CheckpointOptions {
330    /// Human-readable label for the checkpoint.
331    pub name: Option<String>,
332    /// Retention override; must be positive when given.
333    pub ttl: Option<std::time::Duration>,
334}
335
336/// Backends report kernel spellings (`x86_64`, `aarch64`); the SDK speaks the
337/// create-side vocabulary (`amd64`, `arm64`).
338fn normalized_architecture<'de, D: serde::Deserializer<'de>>(
339    deserializer: D,
340) -> Result<String, D::Error> {
341    let raw = String::deserialize(deserializer)?;
342    Ok(match raw.as_str() {
343        "x86_64" | "amd64" => "amd64".to_string(),
344        "aarch64" | "arm64" => "arm64".to_string(),
345        _ => raw,
346    })
347}
348
349/// Named sailbox resource size. Sailboxes are offered in a small discrete CPU
350/// menu rather than free-form vCPU/memory/disk. Each size sets the vCPU count
351/// plus a default memory and disk ceiling. Billing is by observed usage, so a
352/// larger size is a higher ceiling, not a higher bill.
353#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354pub enum SailboxSize {
355    /// Small (1 vCPU): fastest cold starts, forks, and resumes, with
356    /// lower ceilings that cap what a runaway workload can consume.
357    Small,
358    /// Medium (4 vCPU, default).
359    Medium,
360}
361
362impl SailboxSize {
363    /// The wire string for this size.
364    pub fn as_str(&self) -> &'static str {
365        match self {
366            SailboxSize::Small => "s",
367            SailboxSize::Medium => "m",
368        }
369    }
370
371    /// Parse a wire string, returning `None` for an unsupported size.
372    pub fn parse(s: &str) -> Option<SailboxSize> {
373        match s {
374            "s" => Some(SailboxSize::Small),
375            "m" => Some(SailboxSize::Medium),
376            _ => None,
377        }
378    }
379
380    /// The allowed size labels in ascending order, for error messages and help.
381    pub fn allowed() -> &'static [&'static str] {
382        &["s", "m"]
383    }
384}
385
386impl std::fmt::Display for SailboxSize {
387    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
388        f.write_str(self.as_str())
389    }
390}
391
392impl Serialize for SailboxSize {
393    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
394        serializer.serialize_str(self.as_str())
395    }
396}
397
398/// A running-sailbox handle: the fields needed to exec/file/listener against a
399/// live box. Returned by create/resume/from_checkpoint.
400#[derive(Debug, Clone, Default, Serialize)]
401pub struct SailboxHandle {
402    /// The sailbox's stable identifier.
403    pub sailbox_id: String,
404    /// The caller-supplied sailbox name.
405    pub name: String,
406    /// The sailbox's lifecycle status (for example `running`).
407    pub status: SailboxStatus,
408    /// Network address of the worker hosting the sailbox.
409    pub worker_address: String,
410    /// Endpoint used to open exec/file/listener streams to the box.
411    pub exec_endpoint: String,
412}
413
414/// Read-only snapshot from get/list. Deliberately omits worker_address /
415/// exec_endpoint (the list/get endpoints do not return them).
416#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct SailboxInfo {
418    /// The sailbox's stable identifier.
419    pub sailbox_id: String,
420    /// Identifier of the owning app.
421    pub app_id: String,
422    /// Name of the owning app.
423    pub app_name: String,
424    /// Identifier of the image the sailbox was created from.
425    pub image_id: String,
426    /// The caller-supplied sailbox name.
427    pub name: String,
428    /// The sailbox's lifecycle status (for example `running`).
429    pub status: SailboxStatus,
430    /// Configured memory size, in mebibytes.
431    pub memory_mib: i64,
432    /// Configured number of virtual CPUs.
433    pub vcpu_count: i64,
434    /// Configured state-disk size, in gibibytes.
435    pub state_disk_size_gib: i64,
436    /// Requested CPU allocation, in vCPUs.
437    pub cpu_requested_vcpu: i64,
438    /// Current CPU usage, in vCPUs.
439    pub cpu_used_vcpu: f64,
440    /// Requested memory allocation, in bytes.
441    pub memory_requested_bytes: i64,
442    /// Current memory usage, in bytes.
443    pub memory_used_bytes: i64,
444    /// Requested disk allocation, in bytes.
445    pub disk_requested_bytes: i64,
446    /// Current disk usage, in bytes.
447    pub disk_used_bytes: i64,
448    /// CPU architecture of the sailbox, in the create-side vocabulary
449    /// (`amd64` or `arm64`).
450    #[serde(deserialize_with = "normalized_architecture", default)]
451    pub architecture: String,
452    /// Guest schema version the box is running, when reported by the backend.
453    #[serde(default)]
454    pub guest_schema_version: Option<i64>,
455    /// Human-readable error detail, present when the box is in an error state.
456    #[serde(default)]
457    pub error_message: Option<String>,
458    /// Monotonic checkpoint generation counter for the sailbox.
459    pub checkpoint_generation: i64,
460    /// When the box last started, if it has started.
461    #[serde(with = "time::serde::rfc3339::option", default)]
462    pub started_at: Option<OffsetDateTime>,
463    /// When the most recent checkpoint was taken, if any.
464    #[serde(with = "time::serde::rfc3339::option", default)]
465    pub last_checkpointed_at: Option<OffsetDateTime>,
466    /// When the sailbox was created.
467    #[serde(with = "time::serde::rfc3339")]
468    pub created_at: OffsetDateTime,
469    /// When the sailbox was last updated.
470    #[serde(with = "time::serde::rfc3339")]
471    pub updated_at: OffsetDateTime,
472}
473
474/// One page of list results plus the pagination envelope.
475#[derive(Debug, Clone, Serialize)]
476pub struct SailboxPage {
477    /// The sailboxes in this page.
478    pub items: Vec<SailboxInfo>,
479    /// Maximum number of items requested for this page.
480    pub limit: i64,
481    /// Zero-based offset of the first item in this page.
482    pub offset: i64,
483    /// Total number of sailboxes matching the query across all pages.
484    pub total: i64,
485    /// True when further pages exist beyond this one.
486    pub has_more: bool,
487}
488
489/// Query parameters for [`Client::sailbox_spend`](crate::Client::sailbox_spend).
490#[derive(Debug, Clone, Default)]
491pub struct SailboxSpendQuery {
492    /// Restrict spend to sailboxes owned by this app id.
493    pub app_id: Option<String>,
494    /// Restrict spend to one sailbox id.
495    pub sailbox_id: Option<String>,
496    /// Inclusive lower bound for the spend window. The API defaults to the
497    /// current UTC month when omitted.
498    pub from: Option<OffsetDateTime>,
499    /// Exclusive upper bound for the spend window. The API defaults to now when
500    /// omitted.
501    pub to: Option<OffsetDateTime>,
502}
503
504/// Estimated sailbox spend over a requested time window.
505#[derive(Debug, Clone, Serialize, Deserialize)]
506pub struct SailboxSpendResponse {
507    /// Start of the spend window.
508    #[serde(with = "time::serde::rfc3339")]
509    pub start_at: OffsetDateTime,
510    /// End of the spend window.
511    #[serde(with = "time::serde::rfc3339")]
512    pub end_at: OffsetDateTime,
513    /// Cost from closed billing segments, in USD nanos.
514    pub finalized_cost_usd_nanos: i64,
515    /// Estimated cost from still-open billing segments, in USD nanos.
516    pub estimated_active_cost_usd_nanos: i64,
517    /// Total estimated cost, in USD nanos.
518    pub estimated_total_cost_usd_nanos: i64,
519    /// Total observed duration, in seconds.
520    pub duration_seconds: i64,
521    /// Total vCPU-seconds observed.
522    pub vcpu_seconds: f64,
523    /// Total GiB-seconds of memory observed.
524    pub memory_gib_seconds: f64,
525    /// Total GiB-seconds of state disk observed.
526    pub state_disk_gib_seconds: f64,
527    /// Whether non-zero pricing rates were configured by the backend.
528    pub pricing_configured: bool,
529    /// Pricing rates used for the local estimate.
530    pub rates: SailboxSpendRates,
531    /// Per-sailbox spend summaries.
532    pub sailboxes: Vec<SailboxSpendItem>,
533}
534
535/// Rate metadata returned with a sailbox spend estimate.
536#[derive(Debug, Clone, Default, Serialize, Deserialize)]
537pub struct SailboxSpendRates {
538    /// Nanodollars per vCPU-second.
539    pub vcpu_second_usd_nanos: i64,
540    /// Nanodollars per memory GiB-second.
541    pub memory_gib_second_usd_nanos: i64,
542    /// Nanodollars per arm64 state-disk GiB-second.
543    pub state_disk_gib_second_usd_nanos: i64,
544    /// Nanodollars per sailbox creation.
545    pub creation_usd_nanos: i64,
546    /// Nanodollars per amd64 state-disk GiB-second.
547    #[serde(default)]
548    pub amd64_state_disk_gib_second_usd_nanos: i64,
549}
550
551/// Estimated spend for one sailbox over a requested time window.
552#[derive(Debug, Clone, Serialize, Deserialize)]
553pub struct SailboxSpendItem {
554    /// Sailbox id.
555    pub sailbox_id: String,
556    /// Owning app id.
557    pub app_id: String,
558    /// Cost from closed billing segments, in USD nanos.
559    pub finalized_cost_usd_nanos: i64,
560    /// Estimated cost from still-open billing segments, in USD nanos.
561    pub estimated_active_cost_usd_nanos: i64,
562    /// Total estimated cost, in USD nanos.
563    pub estimated_total_cost_usd_nanos: i64,
564    /// Observed duration, in seconds.
565    pub duration_seconds: i64,
566    /// Observed vCPU-seconds.
567    pub vcpu_seconds: f64,
568    /// Observed GiB-seconds of memory.
569    pub memory_gib_seconds: f64,
570    /// Observed GiB-seconds of state disk.
571    pub state_disk_gib_seconds: f64,
572    /// Whether this row includes an active billing segment.
573    pub active: bool,
574}
575
576/// Query parameters for [`Client::sailbox_metrics`](crate::Client::sailbox_metrics).
577#[derive(Debug, Clone)]
578pub struct SailboxMetricsQuery {
579    /// Window name accepted by the API, for example `1h`, `6h`, `24h`, or `7d`.
580    pub range: String,
581}
582
583impl Default for SailboxMetricsQuery {
584    fn default() -> SailboxMetricsQuery {
585        SailboxMetricsQuery {
586            range: "24h".to_string(),
587        }
588    }
589}
590
591/// Resource-usage time series for one sailbox.
592#[derive(Debug, Clone, Serialize, Deserialize)]
593pub struct SailboxMetricsResponse {
594    /// Echoed range selected by the API.
595    pub range: String,
596    /// Time-ordered metric points.
597    pub data: Vec<SailboxMetricPoint>,
598}
599
600/// One bucket in a sailbox resource-usage time series.
601#[derive(Debug, Clone, Serialize, Deserialize)]
602pub struct SailboxMetricPoint {
603    /// Bucket timestamp.
604    #[serde(with = "time::serde::rfc3339")]
605    pub timestamp: OffsetDateTime,
606    /// Current CPU usage, in vCPUs.
607    pub cpu_used_vcpu: f64,
608    /// Requested CPU allocation, in vCPUs.
609    pub cpu_requested_vcpu: i64,
610    /// Current memory usage, in bytes.
611    pub memory_used_bytes: i64,
612    /// Requested memory allocation, in bytes.
613    pub memory_requested_bytes: i64,
614    /// Current disk usage, in bytes.
615    pub disk_used_bytes: i64,
616    /// Requested disk allocation, in bytes.
617    pub disk_requested_bytes: i64,
618}
619
620/// A durable checkpoint handle. `status` echoes the source sailbox's lifecycle
621/// status after checkpointing (a running box is snapshotted; a paused/sleeping
622/// one returns its existing checkpoint), so the binding can sync its handle.
623#[derive(Debug, Clone, Serialize)]
624pub struct SailboxCheckpoint {
625    /// Stable identifier of the checkpoint.
626    pub checkpoint_id: String,
627    /// Identifier of the sailbox the checkpoint was taken from.
628    pub sailbox_id: String,
629    /// Checkpoint generation counter captured by this checkpoint.
630    pub checkpoint_generation: i64,
631    /// When the handle expires and becomes eligible for garbage collection;
632    /// `None` for a handle that carries no fresh retention bound (a paused or
633    /// sleeping source).
634    #[serde(with = "time::serde::rfc3339::option")]
635    pub expires_at: Option<OffsetDateTime>,
636    /// Source sailbox's lifecycle status after checkpointing.
637    pub status: SailboxStatus,
638}
639
640/// Filters for list/list_page.
641#[derive(Debug, Clone)]
642pub struct ListSailboxesQuery {
643    /// Restrict results to sailboxes owned by the app with this id.
644    pub app_id: Option<String>,
645    /// Restrict results to sailboxes in this lifecycle status.
646    pub status: Option<SailboxStatusFilter>,
647    /// Free-text search filter applied by the backend.
648    pub search: Option<String>,
649    /// Exclude sailboxes whose guest schema version exceeds this value.
650    pub max_guest_schema_version: Option<i64>,
651    /// Ordering to apply before pagination.
652    pub order: SailboxListOrder,
653    /// Maximum number of items to return.
654    pub limit: i64,
655    /// Zero-based offset of the first item to return.
656    pub offset: i64,
657}
658
659/// Default page size for listing, matching the sailbox API's own default. The
660/// API rejects `limit=0`, so the derived all-zero default cannot be used.
661#[doc(hidden)]
662pub const DEFAULT_LIST_LIMIT: i64 = 50;
663
664impl Default for ListSailboxesQuery {
665    fn default() -> ListSailboxesQuery {
666        ListSailboxesQuery {
667            app_id: None,
668            status: None,
669            search: None,
670            max_guest_schema_version: None,
671            order: SailboxListOrder::Activity,
672            limit: DEFAULT_LIST_LIMIT,
673            offset: 0,
674        }
675    }
676}
677
678/// One guest ingress port to reserve at create time.
679#[derive(Debug, Clone, Serialize)]
680pub struct IngressPort {
681    /// Port inside the guest to expose for ingress.
682    pub guest_port: u32,
683    /// Transport protocol for the port (for example `tcp`).
684    pub protocol: IngressProtocol,
685    /// Source addresses allowed to reach the port; empty means all sources are
686    /// allowed. Always sent (even when empty) so the request states it
687    /// explicitly rather than relying on an omitted field.
688    pub allowlist: Vec<String>,
689}
690
691/// One NFS volume mount.
692#[derive(Debug, Clone, Serialize)]
693pub struct VolumeMount {
694    /// Identifier of the NFS volume to mount.
695    pub volume_id: String,
696    /// Absolute path inside the guest where the volume is mounted.
697    pub mount_path: String,
698}
699
700/// A managed NFS volume as returned by the sailbox-volume API. (The local
701/// mount path is a binding-side concern and not part of this wire type.)
702#[derive(Debug, Clone, Serialize, Deserialize)]
703pub struct VolumeInfo {
704    /// Stable identifier of the volume.
705    pub volume_id: String,
706    /// Caller-supplied volume name.
707    pub name: String,
708    /// Storage backend serving the volume.
709    pub backend: String,
710    /// Lifecycle status of the volume.
711    pub status: String,
712    /// When the volume was created, if reported.
713    #[serde(with = "time::serde::rfc3339::option", default)]
714    pub created_at: Option<OffsetDateTime>,
715    /// When the volume was last updated, if reported.
716    #[serde(with = "time::serde::rfc3339::option", default)]
717    pub updated_at: Option<OffsetDateTime>,
718}
719
720/// The wire shape of the add-listener response: the scheduler resolves the
721/// public endpoint in the response (so a caller renders it without a second
722/// lookup) but reports nothing about routing, so it converts to a [`Listener`]
723/// with an unspecified route status.
724#[derive(Debug, Clone, Deserialize)]
725pub(crate) struct AddListenerWire {
726    pub(crate) guest_port: u32,
727    pub(crate) protocol: ListenerProtocol,
728    #[serde(default)]
729    pub(crate) public_url: String,
730    #[serde(default, rename = "tcp_public_host")]
731    pub(crate) public_host: String,
732    #[serde(default, rename = "tcp_public_port")]
733    pub(crate) public_port: u32,
734}
735
736impl From<AddListenerWire> for crate::worker::Listener {
737    fn from(wire: AddListenerWire) -> crate::worker::Listener {
738        crate::worker::Listener {
739            guest_port: wire.guest_port,
740            protocol: wire.protocol,
741            // The expose response says nothing about reachability; confirm
742            // with wait_for_listener or re-fetch via get/list.
743            route_status: ListenerRouteStatus::Unspecified,
744            public_url: wire.public_url,
745            public_host: wire.public_host,
746            public_port: wire.public_port,
747        }
748    }
749}
750
751/// A CA-signed SSH user certificate plus the key id that identifies the signing
752/// org (`org=<id>;fp=...;iat=...`).
753#[derive(Debug, Clone)]
754pub struct IssuedUserCert {
755    /// The OpenSSH certificate (`<type>-cert-v01@openssh.com AAAA...`).
756    pub certificate: String,
757    /// The certificate key id.
758    pub key_id: String,
759}
760
761/// How to reach an exposed listener: a routable HTTPS URL for `http`
762/// listeners, or a host/port any TCP client can dial for `tcp` listeners.
763#[derive(Debug, Clone, PartialEq, Eq)]
764pub enum ListenerEndpoint {
765    /// The HTTPS URL to reach the guest service.
766    Http {
767        /// Routable URL.
768        url: String,
769    },
770    /// The address to dial for a raw-TCP listener.
771    Tcp {
772        /// Hostname to dial.
773        host: String,
774        /// Port to dial.
775        port: u32,
776    },
777}
778
779impl std::fmt::Display for ListenerEndpoint {
780    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
781        match self {
782            ListenerEndpoint::Http { url } => f.write_str(url),
783            ListenerEndpoint::Tcp { host, port } => write!(f, "{host}:{port}"),
784        }
785    }
786}
787
788/// Inputs to create a sailbox. The image is a typed [`ImageSpec`](crate::image::ImageSpec);
789/// build one fluently with [`ImageDefinition`](crate::imagebuild::ImageDefinition).
790#[derive(Debug, Clone)]
791pub struct CreateSailboxRequest {
792    /// Identifier of the app that will own the sailbox.
793    pub app_id: String,
794    /// Caller-supplied name for the sailbox.
795    pub name: String,
796    /// Guest ingress ports to reserve at create time.
797    pub ingress_ports: Vec<IngressPort>,
798    /// NFS volumes to mount into the guest.
799    pub volume_mounts: Vec<VolumeMount>,
800    /// The image to create the sailbox from. Serialized to canonical proto-JSON
801    /// and sent as the `image` field.
802    pub image: crate::image::ImageSpec,
803    /// Requested resource size; defaults server-side when absent.
804    pub size: Option<SailboxSize>,
805    /// Optional memory limit in whole GiB, within the size's range; the
806    /// size's default when absent.
807    pub memory_gib: Option<u32>,
808    /// Optional disk size in whole GiB, within the size's range; the size's
809    /// default when absent.
810    pub disk_gib: Option<u32>,
811    /// Optional idle timeout in minutes after which Sail may sleep the
812    /// sailbox.
813    pub autosleep_timeout_minutes: Option<i64>,
814    /// Enable SSH on the new sailbox after create: trust the org SSH CA, start
815    /// `sshd`, and expose guest port 22 as TCP once the CA-only daemon owns it.
816    /// An explicit port-22 ingress entry contributes just its allowlist.
817    pub ssh: bool,
818}
819
820impl Default for CreateSailboxRequest {
821    /// An empty request for a plain Debian-base sailbox: fill in `app_id` and
822    /// `name`, override the rest as needed.
823    fn default() -> CreateSailboxRequest {
824        CreateSailboxRequest {
825            app_id: String::new(),
826            name: String::new(),
827            ingress_ports: Vec::new(),
828            volume_mounts: Vec::new(),
829            image: crate::image::ImageSpec {
830                base: Some(crate::image::BaseImage::Debian),
831                ..crate::image::ImageSpec::default()
832            },
833            size: None,
834            memory_gib: None,
835            disk_gib: None,
836            autosleep_timeout_minutes: None,
837            ssh: false,
838        }
839    }
840}
841
842#[cfg(test)]
843mod tests {
844    use super::*;
845
846    #[test]
847    fn list_query_default_uses_a_valid_nonzero_limit() {
848        // The sailbox API rejects limit=0, so the default must be the API's own
849        // page size, not the derived zero.
850        let query = ListSailboxesQuery::default();
851        assert_eq!(query.limit, DEFAULT_LIST_LIMIT);
852        assert!(query.limit > 0);
853        assert_eq!(query.offset, 0);
854    }
855}