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 /// The user whose credential created this box (for a fork or restore, the
473 /// user who ran it). `None` for service-key creates.
474 #[serde(default)]
475 pub created_by_user_id: Option<String>,
476 /// Access visibility: `Some("private")` restricts exec/files/SSH and
477 /// lifecycle to the creator; `None`/`Some("org")` is org-wide access.
478 #[serde(default)]
479 pub visibility: Option<String>,
480}
481
482/// One page of list results plus the pagination envelope.
483#[derive(Debug, Clone, Serialize)]
484pub struct SailboxPage {
485 /// The sailboxes in this page.
486 pub items: Vec<SailboxInfo>,
487 /// Maximum number of items requested for this page.
488 pub limit: i64,
489 /// Zero-based offset of the first item in this page.
490 pub offset: i64,
491 /// Total number of sailboxes matching the query across all pages.
492 pub total: i64,
493 /// True when further pages exist beyond this one.
494 pub has_more: bool,
495}
496
497/// Query parameters for [`Client::sailbox_spend`](crate::Client::sailbox_spend).
498#[derive(Debug, Clone, Default)]
499pub struct SailboxSpendQuery {
500 /// Restrict spend to sailboxes owned by this app id.
501 pub app_id: Option<String>,
502 /// Restrict spend to one sailbox id.
503 pub sailbox_id: Option<String>,
504 /// Inclusive lower bound for the spend window. The API defaults to the
505 /// current UTC month when omitted.
506 pub from: Option<OffsetDateTime>,
507 /// Exclusive upper bound for the spend window. The API defaults to now when
508 /// omitted.
509 pub to: Option<OffsetDateTime>,
510}
511
512/// Estimated sailbox spend over a requested time window.
513#[derive(Debug, Clone, Serialize, Deserialize)]
514pub struct SailboxSpendResponse {
515 /// Start of the spend window.
516 #[serde(with = "time::serde::rfc3339")]
517 pub start_at: OffsetDateTime,
518 /// End of the spend window.
519 #[serde(with = "time::serde::rfc3339")]
520 pub end_at: OffsetDateTime,
521 /// Cost from closed billing segments, in USD nanos.
522 pub finalized_cost_usd_nanos: i64,
523 /// Estimated cost from still-open billing segments, in USD nanos.
524 pub estimated_active_cost_usd_nanos: i64,
525 /// Total estimated cost, in USD nanos.
526 pub estimated_total_cost_usd_nanos: i64,
527 /// Total observed duration, in seconds.
528 pub duration_seconds: i64,
529 /// Total vCPU-seconds observed.
530 pub vcpu_seconds: f64,
531 /// Total GiB-seconds of memory observed.
532 pub memory_gib_seconds: f64,
533 /// Total GiB-seconds of state disk observed.
534 pub state_disk_gib_seconds: f64,
535 /// Whether non-zero pricing rates were configured by the backend.
536 pub pricing_configured: bool,
537 /// Pricing rates used for the local estimate.
538 pub rates: SailboxSpendRates,
539 /// Per-sailbox spend summaries.
540 pub sailboxes: Vec<SailboxSpendItem>,
541}
542
543/// Rate metadata returned with a sailbox spend estimate.
544#[derive(Debug, Clone, Default, Serialize, Deserialize)]
545pub struct SailboxSpendRates {
546 /// Nanodollars per vCPU-second.
547 pub vcpu_second_usd_nanos: i64,
548 /// Nanodollars per memory GiB-second.
549 pub memory_gib_second_usd_nanos: i64,
550 /// Nanodollars per arm64 state-disk GiB-second.
551 pub state_disk_gib_second_usd_nanos: i64,
552 /// Nanodollars per sailbox creation.
553 pub creation_usd_nanos: i64,
554 /// Nanodollars per amd64 state-disk GiB-second.
555 #[serde(default)]
556 pub amd64_state_disk_gib_second_usd_nanos: i64,
557}
558
559/// Estimated spend for one sailbox over a requested time window.
560#[derive(Debug, Clone, Serialize, Deserialize)]
561pub struct SailboxSpendItem {
562 /// Sailbox id.
563 pub sailbox_id: String,
564 /// Owning app id.
565 pub app_id: String,
566 /// Cost from closed billing segments, in USD nanos.
567 pub finalized_cost_usd_nanos: i64,
568 /// Estimated cost from still-open billing segments, in USD nanos.
569 pub estimated_active_cost_usd_nanos: i64,
570 /// Total estimated cost, in USD nanos.
571 pub estimated_total_cost_usd_nanos: i64,
572 /// Observed duration, in seconds.
573 pub duration_seconds: i64,
574 /// Observed vCPU-seconds.
575 pub vcpu_seconds: f64,
576 /// Observed GiB-seconds of memory.
577 pub memory_gib_seconds: f64,
578 /// Observed GiB-seconds of state disk.
579 pub state_disk_gib_seconds: f64,
580 /// Whether this row includes an active billing segment.
581 pub active: bool,
582}
583
584/// Query parameters for [`Client::sailbox_metrics`](crate::Client::sailbox_metrics).
585#[derive(Debug, Clone)]
586pub struct SailboxMetricsQuery {
587 /// Window name accepted by the API, for example `1h`, `6h`, `24h`, or `7d`.
588 pub range: String,
589}
590
591impl Default for SailboxMetricsQuery {
592 fn default() -> SailboxMetricsQuery {
593 SailboxMetricsQuery {
594 range: "24h".to_string(),
595 }
596 }
597}
598
599/// Resource-usage time series for one sailbox.
600#[derive(Debug, Clone, Serialize, Deserialize)]
601pub struct SailboxMetricsResponse {
602 /// Echoed range selected by the API.
603 pub range: String,
604 /// Time-ordered metric points.
605 pub data: Vec<SailboxMetricPoint>,
606}
607
608/// One bucket in a sailbox resource-usage time series.
609#[derive(Debug, Clone, Serialize, Deserialize)]
610pub struct SailboxMetricPoint {
611 /// Bucket timestamp.
612 #[serde(with = "time::serde::rfc3339")]
613 pub timestamp: OffsetDateTime,
614 /// Current CPU usage, in vCPUs.
615 pub cpu_used_vcpu: f64,
616 /// Requested CPU allocation, in vCPUs.
617 pub cpu_requested_vcpu: i64,
618 /// Current memory usage, in bytes.
619 pub memory_used_bytes: i64,
620 /// Requested memory allocation, in bytes.
621 pub memory_requested_bytes: i64,
622 /// Current disk usage, in bytes.
623 pub disk_used_bytes: i64,
624 /// Requested disk allocation, in bytes.
625 pub disk_requested_bytes: i64,
626}
627
628/// A durable checkpoint handle. `status` echoes the source sailbox's lifecycle
629/// status after checkpointing (a running box is snapshotted; a paused/sleeping
630/// one returns its existing checkpoint), so the binding can sync its handle.
631#[derive(Debug, Clone, Serialize)]
632pub struct SailboxCheckpoint {
633 /// Stable identifier of the checkpoint.
634 pub checkpoint_id: String,
635 /// Identifier of the sailbox the checkpoint was taken from.
636 pub sailbox_id: String,
637 /// Checkpoint generation counter captured by this checkpoint.
638 pub checkpoint_generation: i64,
639 /// When the handle expires and becomes eligible for garbage collection;
640 /// `None` for a handle that carries no fresh retention bound (a paused or
641 /// sleeping source).
642 #[serde(with = "time::serde::rfc3339::option")]
643 pub expires_at: Option<OffsetDateTime>,
644 /// Source sailbox's lifecycle status after checkpointing.
645 pub status: SailboxStatus,
646}
647
648/// Filters for list/list_page.
649#[derive(Debug, Clone)]
650pub struct ListSailboxesQuery {
651 /// Restrict results to sailboxes owned by the app with this id.
652 pub app_id: Option<String>,
653 /// Restrict results to sailboxes in this lifecycle status.
654 pub status: Option<SailboxStatusFilter>,
655 /// Free-text search filter applied by the backend.
656 pub search: Option<String>,
657 /// Exclude sailboxes whose guest schema version exceeds this value.
658 pub max_guest_schema_version: Option<i64>,
659 /// Ordering to apply before pagination.
660 pub order: SailboxListOrder,
661 /// Maximum number of items to return.
662 pub limit: i64,
663 /// Zero-based offset of the first item to return.
664 pub offset: i64,
665}
666
667/// Default page size for listing, matching the sailbox API's own default. The
668/// API rejects `limit=0`, so the derived all-zero default cannot be used.
669#[doc(hidden)]
670pub const DEFAULT_LIST_LIMIT: i64 = 50;
671
672impl Default for ListSailboxesQuery {
673 fn default() -> ListSailboxesQuery {
674 ListSailboxesQuery {
675 app_id: None,
676 status: None,
677 search: None,
678 max_guest_schema_version: None,
679 order: SailboxListOrder::Activity,
680 limit: DEFAULT_LIST_LIMIT,
681 offset: 0,
682 }
683 }
684}
685
686/// One guest ingress port to reserve at create time.
687#[derive(Debug, Clone, Serialize)]
688pub struct IngressPort {
689 /// Port inside the guest to expose for ingress.
690 pub guest_port: u32,
691 /// Transport protocol for the port (for example `tcp`).
692 pub protocol: IngressProtocol,
693 /// Source addresses allowed to reach the port; empty means all sources are
694 /// allowed. Always sent (even when empty) so the request states it
695 /// explicitly rather than relying on an omitted field.
696 pub allowlist: Vec<String>,
697}
698
699/// One NFS volume mount.
700#[derive(Debug, Clone, Serialize)]
701pub struct VolumeMount {
702 /// Identifier of the NFS volume to mount.
703 pub volume_id: String,
704 /// Absolute path inside the guest where the volume is mounted.
705 pub mount_path: String,
706}
707
708/// A managed NFS volume as returned by the sailbox-volume API. (The local
709/// mount path is a binding-side concern and not part of this wire type.)
710#[derive(Debug, Clone, Serialize, Deserialize)]
711pub struct VolumeInfo {
712 /// Stable identifier of the volume.
713 pub volume_id: String,
714 /// Caller-supplied volume name.
715 pub name: String,
716 /// Storage backend serving the volume.
717 pub backend: String,
718 /// Lifecycle status of the volume.
719 pub status: String,
720 /// When the volume was created, if reported.
721 #[serde(with = "time::serde::rfc3339::option", default)]
722 pub created_at: Option<OffsetDateTime>,
723 /// When the volume was last updated, if reported.
724 #[serde(with = "time::serde::rfc3339::option", default)]
725 pub updated_at: Option<OffsetDateTime>,
726}
727
728/// The wire shape of the add-listener response: the scheduler resolves the
729/// public endpoint in the response (so a caller renders it without a second
730/// lookup) but reports nothing about routing, so it converts to a [`Listener`]
731/// with an unspecified route status.
732#[derive(Debug, Clone, Deserialize)]
733pub(crate) struct AddListenerWire {
734 pub(crate) guest_port: u32,
735 pub(crate) protocol: ListenerProtocol,
736 #[serde(default)]
737 pub(crate) public_url: String,
738 #[serde(default, rename = "tcp_public_host")]
739 pub(crate) public_host: String,
740 #[serde(default, rename = "tcp_public_port")]
741 pub(crate) public_port: u32,
742}
743
744impl From<AddListenerWire> for crate::worker::Listener {
745 fn from(wire: AddListenerWire) -> crate::worker::Listener {
746 crate::worker::Listener {
747 guest_port: wire.guest_port,
748 protocol: wire.protocol,
749 // The expose response says nothing about reachability; confirm
750 // with wait_for_listener or re-fetch via get/list.
751 route_status: ListenerRouteStatus::Unspecified,
752 public_url: wire.public_url,
753 public_host: wire.public_host,
754 public_port: wire.public_port,
755 }
756 }
757}
758
759/// A CA-signed SSH user certificate plus the key id that identifies the signing
760/// org (`org=<id>;fp=...;iat=...`).
761#[derive(Debug, Clone)]
762pub struct IssuedUserCert {
763 /// The OpenSSH certificate (`<type>-cert-v01@openssh.com AAAA...`).
764 pub certificate: String,
765 /// The certificate key id.
766 pub key_id: String,
767}
768
769/// How to reach an exposed listener: a routable HTTPS URL for `http`
770/// listeners, or a host/port any TCP client can dial for `tcp` listeners.
771#[derive(Debug, Clone, PartialEq, Eq)]
772pub enum ListenerEndpoint {
773 /// The HTTPS URL to reach the guest service.
774 Http {
775 /// Routable URL.
776 url: String,
777 },
778 /// The address to dial for a raw-TCP listener.
779 Tcp {
780 /// Hostname to dial.
781 host: String,
782 /// Port to dial.
783 port: u32,
784 },
785}
786
787impl std::fmt::Display for ListenerEndpoint {
788 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
789 match self {
790 ListenerEndpoint::Http { url } => f.write_str(url),
791 ListenerEndpoint::Tcp { host, port } => write!(f, "{host}:{port}"),
792 }
793 }
794}
795
796/// Inputs to create a sailbox. The image is a typed [`ImageSpec`](crate::image::ImageSpec);
797/// build one fluently with [`ImageDefinition`](crate::imagebuild::ImageDefinition).
798#[derive(Debug, Clone)]
799pub struct CreateSailboxRequest {
800 /// Identifier of the app that will own the sailbox.
801 pub app_id: String,
802 /// Caller-supplied name for the sailbox.
803 pub name: String,
804 /// Guest ingress ports to reserve at create time.
805 pub ingress_ports: Vec<IngressPort>,
806 /// NFS volumes to mount into the guest.
807 pub volume_mounts: Vec<VolumeMount>,
808 /// The image to create the sailbox from. Serialized to canonical proto-JSON
809 /// and sent as the `image` field.
810 pub image: crate::image::ImageSpec,
811 /// Requested resource size; defaults server-side when absent.
812 pub size: Option<SailboxSize>,
813 /// Optional memory limit in whole GiB, within the size's range; the
814 /// size's default when absent.
815 pub memory_gib: Option<u32>,
816 /// Optional disk size in whole GiB, within the size's range; the size's
817 /// default when absent.
818 pub disk_gib: Option<u32>,
819 /// Enable SSH on the new sailbox after create: trust the org SSH CA, start
820 /// `sshd`, and expose guest port 22 as TCP once the CA-only daemon owns it.
821 /// An explicit port-22 ingress entry contributes just its allowlist.
822 pub ssh: bool,
823 /// By default a sailbox is org-wide: any credential in the org can exec,
824 /// copy files, SSH, or run lifecycle operations on it. `true` restricts all
825 /// of that to the creating user. An org admin can override that with a
826 /// recorded reason for exec, files, and pause/resume/terminate/upgrade. SSH,
827 /// exposing or removing listeners, and fork/checkpoint/restore stay
828 /// creator-only. Requires a user-scoped API key.
829 pub private: bool,
830}
831
832impl Default for CreateSailboxRequest {
833 /// An empty request for a plain Debian-base sailbox: fill in `app_id` and
834 /// `name`, override the rest as needed.
835 fn default() -> CreateSailboxRequest {
836 CreateSailboxRequest {
837 app_id: String::new(),
838 name: String::new(),
839 ingress_ports: Vec::new(),
840 volume_mounts: Vec::new(),
841 image: crate::image::ImageSpec {
842 base: Some(crate::image::BaseImage::Debian),
843 ..crate::image::ImageSpec::default()
844 },
845 size: None,
846 memory_gib: None,
847 disk_gib: None,
848 ssh: false,
849 private: false,
850 }
851 }
852}
853
854#[cfg(test)]
855mod tests {
856 use super::*;
857
858 #[test]
859 fn list_query_default_uses_a_valid_nonzero_limit() {
860 // The sailbox API rejects limit=0, so the default must be the API's own
861 // page size, not the derived zero.
862 let query = ListSailboxesQuery::default();
863 assert_eq!(query.limit, DEFAULT_LIST_LIMIT);
864 assert!(query.limit > 0);
865 assert_eq!(query.offset, 0);
866 }
867}