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, UpgradeOutcome};
use crate::sailbox::types::{
AddListenerResponse, CreateSailboxRequest, ListQuery, NfsVolume, SailboxCheckpoint,
SailboxHandle, SailboxInfo, SailboxPage,
};
use crate::worker::{FileReader, FileWriter, UploadOptions, WorkerProxy};
#[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>,
}
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 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,
None,
)?;
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,
create_timeout: Option<Duration>,
) -> Result<SailboxHandle, SailError> {
self.sailbox_api().create(req, create_timeout).await
}
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: &ListQuery) -> Result<SailboxPage, SailError> {
self.sailbox_api().list(query).await
}
pub async fn terminate_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
self.sailbox_api().terminate(sailbox_id).await
}
pub async fn pause_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
self.sailbox_api().pause(sailbox_id).await
}
pub async fn sleep_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
self.sailbox_api().sleep(sailbox_id).await
}
pub async fn resume_sailbox(&self, sailbox_id: &str) -> Result<SailboxHandle, SailError> {
self.sailbox_api().resume(sailbox_id).await
}
pub async fn checkpoint_sailbox(
&self,
sailbox_id: &str,
) -> Result<SailboxCheckpoint, SailError> {
self.sailbox_api().checkpoint(sailbox_id).await
}
pub async fn create_from_checkpoint(
&self,
checkpoint_id: &str,
name: Option<&str>,
timeout_seconds: Option<i64>,
) -> Result<SailboxHandle, SailError> {
self.sailbox_api()
.from_checkpoint(checkpoint_id, name, timeout_seconds)
.await
}
pub async fn upgrade_sailbox(&self, sailbox_id: &str) -> Result<UpgradeOutcome, SailError> {
self.sailbox_api().upgrade(sailbox_id).await
}
pub async fn expose_listener(
&self,
sailbox_id: &str,
guest_port: u32,
protocol: crate::sailbox::types::IngressProtocol,
allowlist: &[String],
) -> Result<AddListenerResponse, SailError> {
self.sailbox_api()
.expose(sailbox_id, guest_port, protocol, allowlist)
.await
}
pub async fn unexpose_listener(
&self,
sailbox_id: &str,
guest_port: u32,
) -> Result<(), SailError> {
self.sailbox_api().unexpose(sailbox_id, guest_port).await
}
pub async fn list_listeners(
&self,
sailbox_id: &str,
) -> Result<Vec<crate::worker::Listener>, SailError> {
self.sailbox_api().list_listeners(sailbox_id).await
}
pub async fn get_listener(
&self,
sailbox_id: &str,
guest_port: u32,
) -> Result<crate::worker::Listener, SailError> {
self.sailbox_api()
.get_listener(sailbox_id, guest_port)
.await
}
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<f64>,
) -> Result<crate::sailbox::types::IssuedUserCert, SailError> {
self.sailbox_api()
.issue_user_cert(public_key, timeout)
.await
}
pub async fn get_volume(
&self,
name: &str,
mint_if_missing: bool,
) -> Result<NfsVolume, SailError> {
self.sailbox_api().get_volume(name, mint_if_missing).await
}
pub async fn list_volumes(
&self,
max_objects: Option<i64>,
) -> Result<Vec<NfsVolume>, SailError> {
self.sailbox_api().list_volumes(max_objects).await
}
pub async fn delete_volume(
&self,
volume_id: &str,
allow_missing: bool,
) -> Result<Option<NfsVolume>, 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
}
pub(crate) 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)
}
pub async fn exec(
&self,
sailbox_id: &str,
argv: Vec<String>,
options: ExecOptions,
) -> Result<ExecProcess, SailError> {
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,
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
}
pub async fn download_file(
&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))
}
pub async fn upload_file(
&self,
sailbox_id: &str,
remote_path: &str,
options: UploadOptions,
) -> 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,
))
}
}