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