use std::time::Duration;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::error::SailError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SailboxStatus {
Running,
Paused,
Sleeping,
Failed,
Terminated,
Other(String),
}
impl Default for SailboxStatus {
fn default() -> Self {
SailboxStatus::Other(String::new())
}
}
impl SailboxStatus {
pub fn as_str(&self) -> &str {
match self {
SailboxStatus::Running => "running",
SailboxStatus::Paused => "paused",
SailboxStatus::Sleeping => "sleeping",
SailboxStatus::Failed => "failed",
SailboxStatus::Terminated => "terminated",
SailboxStatus::Other(s) => s,
}
}
}
impl From<&str> for SailboxStatus {
fn from(s: &str) -> SailboxStatus {
match s {
"running" => SailboxStatus::Running,
"paused" => SailboxStatus::Paused,
"sleeping" => SailboxStatus::Sleeping,
"failed" => SailboxStatus::Failed,
"terminated" => SailboxStatus::Terminated,
other => SailboxStatus::Other(other.to_string()),
}
}
}
impl std::fmt::Display for SailboxStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl Serialize for SailboxStatus {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for SailboxStatus {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<SailboxStatus, D::Error> {
Ok(SailboxStatus::from(
String::deserialize(deserializer)?.as_str(),
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SailboxStatusFilter {
Running,
Paused,
Sleeping,
Failed,
Terminated,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SailboxListOrder {
#[default]
NewestActive,
NewestCreated,
}
impl SailboxListOrder {
pub fn as_str(&self) -> &'static str {
match self {
SailboxListOrder::NewestActive => "newest_active",
SailboxListOrder::NewestCreated => "newest_created",
}
}
pub fn parse(raw: &str) -> Option<SailboxListOrder> {
match raw.trim() {
"newest_active" => Some(SailboxListOrder::NewestActive),
"newest_created" => Some(SailboxListOrder::NewestCreated),
_ => None,
}
}
}
impl std::str::FromStr for SailboxListOrder {
type Err = SailError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
SailboxListOrder::parse(s).ok_or_else(|| SailError::InvalidArgument {
message: format!(
"unknown list order {s:?}; use \"newest_active\" or \"newest_created\""
),
})
}
}
impl std::fmt::Display for SailboxListOrder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl SailboxStatusFilter {
pub fn as_str(&self) -> &'static str {
match self {
SailboxStatusFilter::Running => "running",
SailboxStatusFilter::Paused => "paused",
SailboxStatusFilter::Sleeping => "sleeping",
SailboxStatusFilter::Failed => "failed",
SailboxStatusFilter::Terminated => "terminated",
}
}
pub fn parse(s: &str) -> Option<SailboxStatusFilter> {
match s {
"running" => Some(SailboxStatusFilter::Running),
"paused" => Some(SailboxStatusFilter::Paused),
"sleeping" => Some(SailboxStatusFilter::Sleeping),
"failed" => Some(SailboxStatusFilter::Failed),
"terminated" => Some(SailboxStatusFilter::Terminated),
_ => None,
}
}
}
impl std::str::FromStr for SailboxStatusFilter {
type Err = SailError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
SailboxStatusFilter::parse(s).ok_or_else(|| SailError::InvalidArgument {
message: format!(
"unknown status filter {s:?}; use \"running\", \"paused\", \"sleeping\", \
\"failed\", or \"terminated\""
),
})
}
}
impl std::fmt::Display for SailboxStatusFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ListenerProtocol {
Tcp,
Http,
Other(String),
}
impl ListenerProtocol {
pub fn as_str(&self) -> &str {
match self {
ListenerProtocol::Tcp => "tcp",
ListenerProtocol::Http => "http",
ListenerProtocol::Other(s) => s,
}
}
}
impl From<&str> for ListenerProtocol {
fn from(s: &str) -> ListenerProtocol {
match s {
"tcp" => ListenerProtocol::Tcp,
"http" => ListenerProtocol::Http,
other => ListenerProtocol::Other(other.to_string()),
}
}
}
impl std::fmt::Display for ListenerProtocol {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl Serialize for ListenerProtocol {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for ListenerProtocol {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<ListenerProtocol, D::Error> {
Ok(ListenerProtocol::from(
String::deserialize(deserializer)?.as_str(),
))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ListenerRouteStatus {
Unknown,
Pending,
Active,
Restoring,
Unavailable,
Other(String),
}
impl ListenerRouteStatus {
pub fn as_str(&self) -> &str {
match self {
ListenerRouteStatus::Unknown => "unknown",
ListenerRouteStatus::Pending => "pending",
ListenerRouteStatus::Active => "active",
ListenerRouteStatus::Restoring => "restoring",
ListenerRouteStatus::Unavailable => "unavailable",
ListenerRouteStatus::Other(s) => s,
}
}
}
impl From<&str> for ListenerRouteStatus {
fn from(s: &str) -> ListenerRouteStatus {
match s {
"LISTENER_ROUTE_STATUS_UNSPECIFIED" | "unknown" => ListenerRouteStatus::Unknown,
"LISTENER_ROUTE_STATUS_PENDING" | "pending" => ListenerRouteStatus::Pending,
"LISTENER_ROUTE_STATUS_ACTIVE" | "active" => ListenerRouteStatus::Active,
"LISTENER_ROUTE_STATUS_RESTORING" | "restoring" => ListenerRouteStatus::Restoring,
"LISTENER_ROUTE_STATUS_UNAVAILABLE" | "unavailable" => ListenerRouteStatus::Unavailable,
other => ListenerRouteStatus::Other(
other
.strip_prefix("LISTENER_ROUTE_STATUS_")
.map_or_else(|| other.to_string(), str::to_ascii_lowercase),
),
}
}
}
impl std::fmt::Display for ListenerRouteStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl Serialize for ListenerRouteStatus {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for ListenerRouteStatus {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<ListenerRouteStatus, D::Error> {
Ok(ListenerRouteStatus::from(
String::deserialize(deserializer)?.as_str(),
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IngressProtocol {
Tcp,
Http,
}
impl IngressProtocol {
pub fn as_str(&self) -> &'static str {
match self {
IngressProtocol::Tcp => "tcp",
IngressProtocol::Http => "http",
}
}
pub fn parse(s: &str) -> Option<IngressProtocol> {
match s {
"tcp" => Some(IngressProtocol::Tcp),
"http" => Some(IngressProtocol::Http),
_ => None,
}
}
}
impl std::str::FromStr for IngressProtocol {
type Err = SailError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
IngressProtocol::parse(s).ok_or_else(|| SailError::InvalidArgument {
message: format!("unknown ingress protocol {s:?}; use \"tcp\" or \"http\""),
})
}
}
impl std::fmt::Display for IngressProtocol {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl Serialize for IngressProtocol {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
#[derive(Debug, Clone, Default)]
pub struct CheckpointOptions {
pub name: Option<String>,
pub ttl: Option<std::time::Duration>,
}
#[derive(Debug, Clone)]
pub struct WaitForListenerOptions {
pub timeout: std::time::Duration,
}
impl Default for WaitForListenerOptions {
fn default() -> WaitForListenerOptions {
WaitForListenerOptions {
timeout: std::time::Duration::from_mins(1),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ForkOptions {
pub name: Option<String>,
pub timeout: Option<std::time::Duration>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SailboxSize {
Small,
Medium,
}
impl SailboxSize {
pub fn as_str(&self) -> &'static str {
match self {
SailboxSize::Small => "s",
SailboxSize::Medium => "m",
}
}
pub fn parse(s: &str) -> Option<SailboxSize> {
match s {
"s" => Some(SailboxSize::Small),
"m" => Some(SailboxSize::Medium),
_ => None,
}
}
pub fn allowed() -> &'static [&'static str] {
&["s", "m"]
}
}
impl std::str::FromStr for SailboxSize {
type Err = SailError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
SailboxSize::parse(s).ok_or_else(|| SailError::InvalidArgument {
message: format!("unknown size {s:?}; use \"s\" or \"m\""),
})
}
}
impl std::fmt::Display for SailboxSize {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl Serialize for SailboxSize {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
#[derive(Debug, Clone, Default, Serialize)]
#[non_exhaustive]
pub struct SailboxHandle {
pub sailbox_id: String,
pub name: String,
pub status: SailboxStatus,
#[doc(hidden)]
pub worker_address: String,
#[doc(hidden)]
pub exec_endpoint: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SailboxInfo {
pub sailbox_id: String,
pub app_id: String,
pub app_name: String,
pub image_id: String,
pub name: String,
pub status: SailboxStatus,
pub memory_mib: i64,
pub vcpu_count: i64,
pub state_disk_size_gib: i64,
pub cpu_requested_vcpu: i64,
pub cpu_used_vcpu: f64,
pub memory_requested_bytes: i64,
pub memory_used_bytes: i64,
pub disk_requested_bytes: i64,
pub disk_used_bytes: i64,
#[serde(default)]
pub architecture: String,
#[serde(default)]
pub guest_schema_version: Option<i64>,
#[serde(default)]
pub error_message: Option<String>,
pub checkpoint_generation: i64,
#[serde(with = "time::serde::rfc3339::option", default)]
pub started_at: Option<OffsetDateTime>,
#[serde(with = "time::serde::rfc3339::option", default)]
pub last_checkpointed_at: Option<OffsetDateTime>,
#[serde(with = "time::serde::rfc3339")]
pub created_at: OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
pub updated_at: OffsetDateTime,
#[serde(default)]
pub created_by_user_id: Option<String>,
#[serde(default)]
pub visibility: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct SailboxPage {
pub items: Vec<SailboxInfo>,
pub limit: i64,
pub offset: i64,
pub total: i64,
pub has_more: bool,
}
#[derive(Debug, Clone, Default)]
pub struct SailboxSpendQuery {
pub app_id: Option<String>,
pub sailbox_id: Option<String>,
pub from: Option<OffsetDateTime>,
pub to: Option<OffsetDateTime>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SailboxSpendResponse {
#[serde(with = "time::serde::rfc3339")]
pub start_at: OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
pub end_at: OffsetDateTime,
pub finalized_cost_usd_nanos: i64,
pub estimated_active_cost_usd_nanos: i64,
pub estimated_total_cost_usd_nanos: i64,
pub duration_seconds: i64,
pub vcpu_seconds: f64,
pub memory_gib_seconds: f64,
pub state_disk_gib_seconds: f64,
pub pricing_configured: bool,
pub rates: SailboxSpendRates,
pub sailboxes: Vec<SailboxSpendItem>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SailboxSpendRates {
pub vcpu_second_usd_nanos: i64,
pub memory_gib_second_usd_nanos: i64,
pub state_disk_gib_second_usd_nanos: i64,
pub creation_usd_nanos: i64,
#[serde(default)]
pub amd64_state_disk_gib_second_usd_nanos: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SailboxSpendItem {
pub sailbox_id: String,
pub app_id: String,
pub finalized_cost_usd_nanos: i64,
pub estimated_active_cost_usd_nanos: i64,
pub estimated_total_cost_usd_nanos: i64,
pub duration_seconds: i64,
pub vcpu_seconds: f64,
pub memory_gib_seconds: f64,
pub state_disk_gib_seconds: f64,
pub active: bool,
}
#[derive(Debug, Clone)]
pub struct SailboxMetricsQuery {
pub range: String,
}
impl Default for SailboxMetricsQuery {
fn default() -> SailboxMetricsQuery {
SailboxMetricsQuery {
range: "24h".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SailboxMetricsResponse {
pub range: String,
pub data: Vec<SailboxMetricPoint>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SailboxMetricPoint {
#[serde(with = "time::serde::rfc3339")]
pub timestamp: OffsetDateTime,
pub cpu_used_vcpu: f64,
pub cpu_requested_vcpu: i64,
pub memory_used_bytes: i64,
pub memory_requested_bytes: i64,
pub disk_used_bytes: i64,
pub disk_requested_bytes: i64,
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct SailboxCheckpoint {
pub checkpoint_id: String,
pub sailbox_id: String,
pub checkpoint_generation: i64,
#[serde(with = "time::serde::rfc3339::option")]
pub expires_at: Option<OffsetDateTime>,
pub status: SailboxStatus,
}
#[derive(Debug, Clone)]
pub struct ListSailboxesQuery {
pub app_id: Option<String>,
pub status: Option<SailboxStatusFilter>,
pub search: Option<String>,
pub order: SailboxListOrder,
pub limit: i64,
pub offset: i64,
}
#[doc(hidden)]
pub const DEFAULT_LIST_LIMIT: i64 = 50;
impl Default for ListSailboxesQuery {
fn default() -> ListSailboxesQuery {
ListSailboxesQuery {
app_id: None,
status: None,
search: None,
order: SailboxListOrder::NewestActive,
limit: DEFAULT_LIST_LIMIT,
offset: 0,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct IngressPort {
pub guest_port: u32,
pub protocol: IngressProtocol,
pub allowlist: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct VolumeMount {
pub volume_id: String,
pub mount_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct VolumeInfo {
pub volume_id: String,
pub name: String,
pub backend: String,
pub status: String,
#[serde(with = "time::serde::rfc3339::option", default)]
pub created_at: Option<OffsetDateTime>,
#[serde(with = "time::serde::rfc3339::option", default)]
pub updated_at: Option<OffsetDateTime>,
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct AddListenerWire {
pub(crate) guest_port: u32,
pub(crate) protocol: ListenerProtocol,
#[serde(default)]
pub(crate) public_url: String,
#[serde(default, rename = "tcp_public_host")]
pub(crate) public_host: String,
#[serde(default, rename = "tcp_public_port")]
pub(crate) public_port: u32,
}
impl From<AddListenerWire> for crate::worker::Listener {
fn from(wire: AddListenerWire) -> crate::worker::Listener {
crate::worker::Listener {
guest_port: wire.guest_port,
protocol: wire.protocol,
route_status: ListenerRouteStatus::Unknown,
public_url: wire.public_url,
public_host: wire.public_host,
public_port: wire.public_port,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct IssuedUserCert {
pub certificate: String,
pub key_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ListenerEndpoint {
Http {
url: String,
},
Tcp {
host: String,
port: u32,
},
}
impl std::fmt::Display for ListenerEndpoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ListenerEndpoint::Http { url } => f.write_str(url),
ListenerEndpoint::Tcp { host, port } => write!(f, "{host}:{port}"),
}
}
}
#[derive(Debug, Clone)]
pub struct CreateSailboxRequest {
pub app_id: String,
pub name: String,
pub ingress_ports: Vec<IngressPort>,
pub volume_mounts: Vec<VolumeMount>,
pub image: crate::image::ImageSpec,
pub size: Option<SailboxSize>,
pub memory_gib: Option<u32>,
pub disk_gib: Option<u32>,
pub ssh: bool,
pub private: bool,
pub image_build_timeout: Option<Duration>,
}
impl Default for CreateSailboxRequest {
fn default() -> CreateSailboxRequest {
CreateSailboxRequest {
app_id: String::new(),
name: String::new(),
ingress_ports: Vec::new(),
volume_mounts: Vec::new(),
image: crate::image::ImageSpec {
base: Some(crate::image::BaseImage::Debian),
..crate::image::ImageSpec::default()
},
size: None,
memory_gib: None,
disk_gib: None,
ssh: false,
private: false,
image_build_timeout: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn closed_enums_round_trip_through_fromstr_and_display() {
for (text, value) in [
("newest_active", SailboxListOrder::NewestActive),
("newest_created", SailboxListOrder::NewestCreated),
] {
assert_eq!(text.parse::<SailboxListOrder>().unwrap(), value);
assert_eq!(value.to_string(), text);
}
for (text, value) in [
("running", SailboxStatusFilter::Running),
("terminated", SailboxStatusFilter::Terminated),
] {
assert_eq!(text.parse::<SailboxStatusFilter>().unwrap(), value);
assert_eq!(value.to_string(), text);
}
for (text, value) in [
("tcp", IngressProtocol::Tcp),
("http", IngressProtocol::Http),
] {
assert_eq!(text.parse::<IngressProtocol>().unwrap(), value);
assert_eq!(value.to_string(), text);
}
for (text, value) in [("s", SailboxSize::Small), ("m", SailboxSize::Medium)] {
assert_eq!(text.parse::<SailboxSize>().unwrap(), value);
assert_eq!(value.to_string(), text);
}
let err = "udp".parse::<IngressProtocol>().unwrap_err();
assert!(matches!(err, SailError::InvalidArgument { .. }));
assert!(err.to_string().contains("udp"));
}
#[test]
fn list_query_default_uses_a_valid_nonzero_limit() {
let query = ListSailboxesQuery::default();
assert_eq!(query.limit, DEFAULT_LIST_LIMIT);
assert!(query.limit > 0);
assert_eq!(query.offset, 0);
}
#[test]
fn route_status_round_trips_through_serde() {
for status in [
ListenerRouteStatus::Unknown,
ListenerRouteStatus::Pending,
ListenerRouteStatus::Active,
ListenerRouteStatus::Restoring,
ListenerRouteStatus::Unavailable,
ListenerRouteStatus::Other("degraded".to_string()),
] {
let json = serde_json::to_string(&status).unwrap();
let back: ListenerRouteStatus = serde_json::from_str(&json).unwrap();
assert_eq!(
status, back,
"round-trip failed for {status:?} (json {json})"
);
}
assert_eq!(
ListenerRouteStatus::from("LISTENER_ROUTE_STATUS_ACTIVE"),
ListenerRouteStatus::Active,
);
}
}