use std::sync::Arc;
use std::time::Duration;
use crate::app::{self, App};
use crate::config::Config;
use crate::error::SailError;
use crate::exec::{ExecOptions, ExecParams, ExecProcess};
use crate::http::HttpCore;
use crate::imagebuilder::ImageBuilder;
use crate::sailbox::api::{SailboxApi, UpgradeResult};
use crate::sailbox::object::Sailbox;
use crate::sailbox::types::{
CreateSailboxRequest, ListSailboxesQuery, SailboxCheckpoint, SailboxHandle, SailboxInfo,
SailboxPage, VolumeInfo,
};
use crate::worker::{FileReader, FileWriter, Listener, WorkerProxy, WriteOptions};
#[derive(Clone)]
pub struct Client {
inner: Arc<Inner>,
}
struct Inner {
config: Config,
sailbox_http: HttpCore,
api_http: HttpCore,
worker: Arc<WorkerProxy>,
imagebuilder: ImageBuilder,
}
#[derive(Debug, Default, Clone)]
pub struct ClientBuilder {
mode: Option<String>,
api_key: Option<String>,
api_url: Option<String>,
sailbox_api_url: Option<String>,
imagebuilder_url: Option<String>,
ingress_url: Option<String>,
}
impl ClientBuilder {
pub fn new(api_key: impl Into<String>) -> ClientBuilder {
ClientBuilder {
api_key: Some(api_key.into()),
..ClientBuilder::default()
}
}
pub fn mode(mut self, mode: impl Into<String>) -> ClientBuilder {
self.mode = Some(mode.into());
self
}
pub fn api_url(mut self, api_url: impl Into<String>) -> ClientBuilder {
self.api_url = Some(api_url.into());
self
}
pub fn sailbox_api_url(mut self, url: impl Into<String>) -> ClientBuilder {
self.sailbox_api_url = Some(url.into());
self
}
pub fn imagebuilder_url(mut self, url: impl Into<String>) -> ClientBuilder {
self.imagebuilder_url = Some(url.into());
self
}
pub fn ingress_url(mut self, url: impl Into<String>) -> ClientBuilder {
self.ingress_url = Some(url.into());
self
}
pub fn build(self) -> Result<Client, SailError> {
let api_key = self.api_key.unwrap_or_default();
let config = Config::resolve(
self.mode.as_deref(),
api_key,
self.api_url,
self.sailbox_api_url,
self.imagebuilder_url,
self.ingress_url,
)?;
Client::from_config(config)
}
}
impl Client {
pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
ClientBuilder::new(api_key)
}
pub fn from_env() -> Result<Client, SailError> {
Client::from_config(Config::from_env()?)
}
pub fn from_config(config: Config) -> Result<Client, SailError> {
let sailbox_http = HttpCore::new(&config.sailbox_api_url, &config.api_key)?;
let api_http = HttpCore::new(&config.api_url, &config.api_key)?;
let worker = Arc::new(WorkerProxy::new(&config.api_key)?);
let imagebuilder = ImageBuilder::new(&config.imagebuilder_url, &config.api_key)?;
Ok(Client {
inner: Arc::new(Inner {
config,
sailbox_http,
api_http,
worker,
imagebuilder,
}),
})
}
pub fn config(&self) -> &Config {
&self.inner.config
}
#[doc(hidden)]
pub fn worker(&self) -> Arc<WorkerProxy> {
Arc::clone(&self.inner.worker)
}
#[doc(hidden)]
pub fn imagebuilder(&self) -> &ImageBuilder {
&self.inner.imagebuilder
}
#[doc(hidden)]
pub fn sailbox_http(&self) -> &HttpCore {
&self.inner.sailbox_http
}
#[doc(hidden)]
pub fn api_http(&self) -> &HttpCore {
&self.inner.api_http
}
fn sailbox_api(&self) -> SailboxApi<'_> {
SailboxApi::new(&self.inner.sailbox_http)
}
pub async fn create_sailbox(
&self,
req: &CreateSailboxRequest,
timeout: Option<Duration>,
) -> Result<Sailbox, SailError> {
let bind = |handle: SailboxHandle| Sailbox::bind(self.clone(), handle);
if !req.ssh {
return self.sailbox_api().create(req, timeout).await.map(bind);
}
crate::sailbox::api::validate_ingress_ports(&req.ingress_ports)?;
self.org_ssh_ca_public_key().await?;
let mut req = req.clone();
let ssh_allowlist = req
.ingress_ports
.iter()
.find(|port| port.guest_port == 22)
.map(|port| port.allowlist.clone())
.unwrap_or_default();
req.ingress_ports.retain(|port| port.guest_port != 22);
let handle = self.sailbox_api().create(&req, timeout).await?;
let handle_id = handle.sailbox_id.clone();
if let Err(err) = self
.enable_ssh(&handle_id, &ssh_allowlist, false, Duration::ZERO)
.await
{
return Err(SailError::Creation {
message: format!(
"sailbox {handle_id} was created, but SSH setup failed: {err}. Fetch it by \
id to retry enable_ssh or terminate it."
),
status: 0,
body: serde_json::Value::Null,
});
}
Ok(bind(handle))
}
#[doc(hidden)]
pub async fn get_sailbox(&self, sailbox_id: &str) -> Result<SailboxInfo, SailError> {
self.sailbox_api().get(sailbox_id).await
}
pub async fn list_sailboxes(
&self,
query: &ListSailboxesQuery,
) -> Result<SailboxPage, SailError> {
self.sailbox_api().list(query).await
}
#[doc(hidden)]
pub async fn terminate_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
self.sailbox_api().terminate(sailbox_id).await
}
#[doc(hidden)]
pub async fn pause_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
self.sailbox_api().pause(sailbox_id).await
}
#[doc(hidden)]
pub async fn sleep_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
self.sailbox_api().sleep(sailbox_id).await
}
#[doc(hidden)]
pub async fn resume_sailbox(&self, sailbox_id: &str) -> Result<SailboxHandle, SailError> {
self.sailbox_api().resume(sailbox_id).await
}
#[doc(hidden)]
pub async fn checkpoint_sailbox(
&self,
sailbox_id: &str,
name: Option<&str>,
ttl_seconds: Option<i64>,
) -> Result<SailboxCheckpoint, SailError> {
self.sailbox_api()
.checkpoint(sailbox_id, name, ttl_seconds)
.await
}
pub async fn create_from_checkpoint(
&self,
checkpoint_id: &str,
name: Option<&str>,
timeout: Option<Duration>,
) -> Result<Sailbox, SailError> {
self.sailbox_api()
.from_checkpoint(checkpoint_id, name, timeout.map(|t| t.as_secs() as i64))
.await
.map(|handle| Sailbox::bind(self.clone(), handle))
}
#[doc(hidden)]
pub async fn upgrade_sailbox(&self, sailbox_id: &str) -> Result<UpgradeResult, SailError> {
self.sailbox_api().upgrade(sailbox_id).await
}
#[doc(hidden)]
pub async fn expose_listener(
&self,
sailbox_id: &str,
guest_port: u32,
protocol: crate::sailbox::types::IngressProtocol,
allowlist: &[String],
) -> Result<Listener, SailError> {
let mut response = self
.sailbox_api()
.expose(sailbox_id, guest_port, protocol, allowlist)
.await?;
self.fill_listener_url(sailbox_id, &mut response);
Ok(response)
}
#[doc(hidden)]
pub async fn unexpose_listener(
&self,
sailbox_id: &str,
guest_port: u32,
) -> Result<(), SailError> {
self.sailbox_api().unexpose(sailbox_id, guest_port).await
}
#[doc(hidden)]
pub async fn list_listeners(
&self,
sailbox_id: &str,
) -> Result<Vec<crate::worker::Listener>, SailError> {
let mut listeners = self.sailbox_api().list_listeners(sailbox_id).await?;
for listener in &mut listeners {
self.fill_listener_url(sailbox_id, listener);
}
Ok(listeners)
}
#[doc(hidden)]
pub async fn get_listener(
&self,
sailbox_id: &str,
guest_port: u32,
) -> Result<crate::worker::Listener, SailError> {
let mut listener = self
.sailbox_api()
.get_listener(sailbox_id, guest_port)
.await?;
self.fill_listener_url(sailbox_id, &mut listener);
Ok(listener)
}
fn fill_listener_url(&self, sailbox_id: &str, listener: &mut crate::worker::Listener) {
if listener.public_url.is_empty()
&& listener.protocol != crate::sailbox::types::ListenerProtocol::Tcp
{
listener.public_url = crate::sailbox::listeners::synthesized_public_url(
self.config(),
sailbox_id,
listener.guest_port,
);
}
}
#[doc(hidden)]
pub async fn ingress_auth_headers(
&self,
sailbox_id: &str,
) -> Result<Vec<(String, String)>, SailError> {
self.sailbox_api().ingress_auth_headers(sailbox_id).await
}
pub async fn org_ssh_ca_public_key(&self) -> Result<String, SailError> {
self.sailbox_api().org_ssh_ca_public_key().await
}
pub async fn issue_user_cert(
&self,
public_key: &str,
timeout: Option<Duration>,
) -> Result<crate::sailbox::types::IssuedUserCert, SailError> {
self.sailbox_api()
.issue_user_cert(public_key, timeout.map(|t| t.as_secs_f64()))
.await
}
pub async fn get_volume(
&self,
name: &str,
mint_if_missing: bool,
) -> Result<VolumeInfo, SailError> {
self.sailbox_api().get_volume(name, mint_if_missing).await
}
pub async fn list_volumes(
&self,
max_objects: Option<i64>,
) -> Result<Vec<VolumeInfo>, SailError> {
self.sailbox_api().list_volumes(max_objects).await
}
pub async fn delete_volume(
&self,
volume_id: &str,
allow_missing: bool,
) -> Result<Option<VolumeInfo>, SailError> {
self.sailbox_api()
.delete_volume(volume_id, allow_missing)
.await
}
pub async fn find_app(&self, name: &str, mint_if_missing: bool) -> Result<App, SailError> {
app::find_app(&self.inner.api_http, name, mint_if_missing).await
}
pub async fn list_apps(&self) -> Result<Vec<App>, SailError> {
app::list_apps(&self.inner.api_http).await
}
#[doc(hidden)]
pub async fn exec_endpoint(&self, sailbox_id: &str) -> Result<String, SailError> {
let handle = self.resume_sailbox(sailbox_id).await?;
if handle.exec_endpoint.is_empty() {
return Err(SailError::Internal {
message: format!("sailbox {sailbox_id} resumed without an exec endpoint"),
});
}
Ok(handle.exec_endpoint)
}
#[doc(hidden)]
pub async fn exec(
&self,
sailbox_id: &str,
argv: Vec<String>,
options: ExecOptions,
) -> Result<ExecProcess, SailError> {
if argv.is_empty() {
return Err(SailError::InvalidArgument {
message: "command must be non-empty".to_string(),
});
}
if options.cwd.is_some() || options.background {
return Err(SailError::InvalidArgument {
message: "cwd and background require a shell command; use exec_shell".to_string(),
});
}
let exec_endpoint = self.exec_endpoint(sailbox_id).await?;
let params = ExecParams {
sailbox_id: sailbox_id.to_string(),
exec_endpoint,
argv,
timeout_seconds: options
.timeout
.map_or(0, |d| d.as_secs_f64().ceil().max(1.0) as u32),
idempotency_key: options.idempotency_key,
open_stdin: options.open_stdin || options.pty,
pty: options.pty,
term: options.term,
cols: options.cols,
rows: options.rows,
retry_timeout: options.retry_timeout.as_secs_f64(),
extra_metadata: Vec::new(),
};
ExecProcess::start(self.worker(), params).await
}
#[doc(hidden)]
pub async fn exec_shell(
&self,
sailbox_id: &str,
command: &str,
mut options: ExecOptions,
) -> Result<ExecProcess, SailError> {
let argv = crate::exec::shell_argv(command, &options)?;
options.cwd = None;
options.background = false;
self.exec(sailbox_id, argv, options).await
}
#[doc(hidden)]
pub async fn read_stream(
&self,
sailbox_id: &str,
remote_path: &str,
) -> Result<FileReader, SailError> {
let endpoint = self.exec_endpoint(sailbox_id).await?;
Ok(self
.inner
.worker
.read_file(&endpoint, sailbox_id, remote_path))
}
#[doc(hidden)]
pub async fn read_file(
&self,
sailbox_id: &str,
remote_path: &str,
) -> Result<Vec<u8>, SailError> {
let reader = self.read_stream(sailbox_id, remote_path).await?;
let mut contents = Vec::new();
while let Some(chunk) = reader.next().await {
contents.extend_from_slice(&chunk?);
}
Ok(contents)
}
#[doc(hidden)]
pub async fn write_stream(
&self,
sailbox_id: &str,
remote_path: &str,
options: WriteOptions,
) -> Result<FileWriter, SailError> {
let endpoint = self.exec_endpoint(sailbox_id).await?;
Ok(self.inner.worker.write_file(
&endpoint,
sailbox_id,
remote_path,
options.create_parents,
options.mode,
))
}
#[doc(hidden)]
pub async fn write_file(
&self,
sailbox_id: &str,
remote_path: &str,
data: &[u8],
options: WriteOptions,
) -> Result<(), SailError> {
let mut writer = self.write_stream(sailbox_id, remote_path, options).await?;
writer.write(data).await?;
writer.finish().await
}
}