1use std::sync::Arc;
30use std::time::Duration;
31
32use crate::app::{self, App};
33use crate::config::Config;
34use crate::error::SailError;
35use crate::exec::{ExecOptions, ExecParams, ExecProcess};
36use crate::http::HttpCore;
37use crate::imagebuilder::ImageBuilder;
38use crate::sailbox::api::{SailboxApi, UpgradeOutcome};
39use crate::sailbox::types::{
40 AddListenerResponse, CreateSailboxRequest, ListQuery, NfsVolume, SailboxCheckpoint,
41 SailboxHandle, SailboxInfo, SailboxPage,
42};
43use crate::worker::{FileReader, FileWriter, UploadOptions, WorkerProxy};
44
45#[derive(Clone)]
47pub struct Client {
48 inner: Arc<Inner>,
49}
50
51struct Inner {
52 config: Config,
53 sailbox_http: HttpCore,
55 api_http: HttpCore,
57 worker: Arc<WorkerProxy>,
60 imagebuilder: ImageBuilder,
61}
62
63#[derive(Debug, Default, Clone)]
66pub struct ClientBuilder {
67 mode: Option<String>,
68 api_key: Option<String>,
69 api_url: Option<String>,
70 sailbox_api_url: Option<String>,
71 imagebuilder_url: Option<String>,
72}
73
74impl ClientBuilder {
75 pub fn new(api_key: impl Into<String>) -> ClientBuilder {
78 ClientBuilder {
79 api_key: Some(api_key.into()),
80 ..ClientBuilder::default()
81 }
82 }
83
84 pub fn mode(mut self, mode: impl Into<String>) -> ClientBuilder {
87 self.mode = Some(mode.into());
88 self
89 }
90
91 pub fn api_url(mut self, api_url: impl Into<String>) -> ClientBuilder {
93 self.api_url = Some(api_url.into());
94 self
95 }
96
97 pub fn sailbox_api_url(mut self, url: impl Into<String>) -> ClientBuilder {
99 self.sailbox_api_url = Some(url.into());
100 self
101 }
102
103 pub fn imagebuilder_url(mut self, url: impl Into<String>) -> ClientBuilder {
105 self.imagebuilder_url = Some(url.into());
106 self
107 }
108
109 pub fn build(self) -> Result<Client, SailError> {
111 let api_key = self.api_key.unwrap_or_default();
112 let config = Config::resolve(
113 self.mode.as_deref(),
114 api_key,
115 self.api_url,
116 self.sailbox_api_url,
117 self.imagebuilder_url,
118 None,
121 )?;
122 Client::from_config(config)
123 }
124}
125
126impl Client {
127 pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
129 ClientBuilder::new(api_key)
130 }
131
132 pub fn from_env() -> Result<Client, SailError> {
134 Client::from_config(Config::from_env()?)
135 }
136
137 pub fn from_config(config: Config) -> Result<Client, SailError> {
139 let sailbox_http = HttpCore::new(&config.sailbox_api_url, &config.api_key)?;
140 let api_http = HttpCore::new(&config.api_url, &config.api_key)?;
141 let worker = Arc::new(WorkerProxy::new(&config.api_key)?);
142 let imagebuilder = ImageBuilder::new(&config.imagebuilder_url, &config.api_key)?;
143 Ok(Client {
144 inner: Arc::new(Inner {
145 config,
146 sailbox_http,
147 api_http,
148 worker,
149 imagebuilder,
150 }),
151 })
152 }
153
154 pub fn config(&self) -> &Config {
156 &self.inner.config
157 }
158
159 #[doc(hidden)]
161 pub fn worker(&self) -> Arc<WorkerProxy> {
162 Arc::clone(&self.inner.worker)
163 }
164
165 #[doc(hidden)]
167 pub fn imagebuilder(&self) -> &ImageBuilder {
168 &self.inner.imagebuilder
169 }
170
171 #[doc(hidden)]
173 pub fn sailbox_http(&self) -> &HttpCore {
174 &self.inner.sailbox_http
175 }
176
177 #[doc(hidden)]
179 pub fn api_http(&self) -> &HttpCore {
180 &self.inner.api_http
181 }
182
183 fn sailbox_api(&self) -> SailboxApi<'_> {
184 SailboxApi::new(&self.inner.sailbox_http)
185 }
186
187 pub async fn create_sailbox(
196 &self,
197 req: &CreateSailboxRequest,
198 create_timeout: Option<Duration>,
199 ) -> Result<SailboxHandle, SailError> {
200 self.sailbox_api().create(req, create_timeout).await
201 }
202
203 pub async fn get_sailbox(&self, sailbox_id: &str) -> Result<SailboxInfo, SailError> {
205 self.sailbox_api().get(sailbox_id).await
206 }
207
208 pub async fn list_sailboxes(&self, query: &ListQuery) -> Result<SailboxPage, SailError> {
210 self.sailbox_api().list(query).await
211 }
212
213 pub async fn terminate_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
215 self.sailbox_api().terminate(sailbox_id).await
216 }
217
218 pub async fn pause_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
220 self.sailbox_api().pause(sailbox_id).await
221 }
222
223 pub async fn sleep_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
225 self.sailbox_api().sleep(sailbox_id).await
226 }
227
228 pub async fn resume_sailbox(&self, sailbox_id: &str) -> Result<SailboxHandle, SailError> {
230 self.sailbox_api().resume(sailbox_id).await
231 }
232
233 pub async fn checkpoint_sailbox(
235 &self,
236 sailbox_id: &str,
237 ) -> Result<SailboxCheckpoint, SailError> {
238 self.sailbox_api().checkpoint(sailbox_id).await
239 }
240
241 pub async fn create_from_checkpoint(
243 &self,
244 checkpoint_id: &str,
245 name: Option<&str>,
246 timeout_seconds: Option<i64>,
247 ) -> Result<SailboxHandle, SailError> {
248 self.sailbox_api()
249 .from_checkpoint(checkpoint_id, name, timeout_seconds)
250 .await
251 }
252
253 pub async fn upgrade_sailbox(&self, sailbox_id: &str) -> Result<UpgradeOutcome, SailError> {
255 self.sailbox_api().upgrade(sailbox_id).await
256 }
257
258 pub async fn expose_listener(
260 &self,
261 sailbox_id: &str,
262 guest_port: u32,
263 protocol: crate::sailbox::types::IngressProtocol,
264 allowlist: &[String],
265 ) -> Result<AddListenerResponse, SailError> {
266 self.sailbox_api()
267 .expose(sailbox_id, guest_port, protocol, allowlist)
268 .await
269 }
270
271 pub async fn unexpose_listener(
273 &self,
274 sailbox_id: &str,
275 guest_port: u32,
276 ) -> Result<(), SailError> {
277 self.sailbox_api().unexpose(sailbox_id, guest_port).await
278 }
279
280 pub async fn list_listeners(
282 &self,
283 sailbox_id: &str,
284 ) -> Result<Vec<crate::worker::Listener>, SailError> {
285 self.sailbox_api().list_listeners(sailbox_id).await
286 }
287
288 pub async fn get_listener(
291 &self,
292 sailbox_id: &str,
293 guest_port: u32,
294 ) -> Result<crate::worker::Listener, SailError> {
295 self.sailbox_api()
296 .get_listener(sailbox_id, guest_port)
297 .await
298 }
299
300 pub async fn ingress_auth_headers(
302 &self,
303 sailbox_id: &str,
304 ) -> Result<Vec<(String, String)>, SailError> {
305 self.sailbox_api().ingress_auth_headers(sailbox_id).await
306 }
307
308 pub async fn org_ssh_ca_public_key(&self) -> Result<String, SailError> {
310 self.sailbox_api().org_ssh_ca_public_key().await
311 }
312
313 pub async fn issue_user_cert(
317 &self,
318 public_key: &str,
319 timeout: Option<f64>,
320 ) -> Result<crate::sailbox::types::IssuedUserCert, SailError> {
321 self.sailbox_api()
322 .issue_user_cert(public_key, timeout)
323 .await
324 }
325
326 pub async fn get_volume(
330 &self,
331 name: &str,
332 mint_if_missing: bool,
333 ) -> Result<NfsVolume, SailError> {
334 self.sailbox_api().get_volume(name, mint_if_missing).await
335 }
336
337 pub async fn list_volumes(
339 &self,
340 max_objects: Option<i64>,
341 ) -> Result<Vec<NfsVolume>, SailError> {
342 self.sailbox_api().list_volumes(max_objects).await
343 }
344
345 pub async fn delete_volume(
347 &self,
348 volume_id: &str,
349 allow_missing: bool,
350 ) -> Result<Option<NfsVolume>, SailError> {
351 self.sailbox_api()
352 .delete_volume(volume_id, allow_missing)
353 .await
354 }
355
356 pub async fn find_app(&self, name: &str, mint_if_missing: bool) -> Result<App, SailError> {
360 app::find_app(&self.inner.api_http, name, mint_if_missing).await
361 }
362
363 pub async fn list_apps(&self) -> Result<Vec<App>, SailError> {
365 app::list_apps(&self.inner.api_http).await
366 }
367
368 pub(crate) async fn exec_endpoint(&self, sailbox_id: &str) -> Result<String, SailError> {
378 let handle = self.resume_sailbox(sailbox_id).await?;
379 if handle.exec_endpoint.is_empty() {
380 return Err(SailError::Internal {
381 message: format!("sailbox {sailbox_id} resumed without an exec endpoint"),
382 });
383 }
384 Ok(handle.exec_endpoint)
385 }
386
387 pub async fn exec(
398 &self,
399 sailbox_id: &str,
400 argv: Vec<String>,
401 options: ExecOptions,
402 ) -> Result<ExecProcess, SailError> {
403 let exec_endpoint = self.exec_endpoint(sailbox_id).await?;
404 let params = ExecParams {
405 sailbox_id: sailbox_id.to_string(),
406 exec_endpoint,
407 argv,
408 timeout_seconds: options
411 .timeout
412 .map_or(0, |d| d.as_secs_f64().ceil().max(1.0) as u32),
413 idempotency_key: options.idempotency_key,
414 open_stdin: options.open_stdin,
415 pty: options.pty,
416 term: options.term,
417 cols: options.cols,
418 rows: options.rows,
419 retry_timeout: options.retry_timeout.as_secs_f64(),
420 extra_metadata: Vec::new(),
421 };
422 ExecProcess::start(self.worker(), params).await
423 }
424
425 pub async fn download_file(
433 &self,
434 sailbox_id: &str,
435 remote_path: &str,
436 ) -> Result<FileReader, SailError> {
437 let endpoint = self.exec_endpoint(sailbox_id).await?;
438 Ok(self
439 .inner
440 .worker
441 .read_file(&endpoint, sailbox_id, remote_path))
442 }
443
444 pub async fn upload_file(
453 &self,
454 sailbox_id: &str,
455 remote_path: &str,
456 options: UploadOptions,
457 ) -> Result<FileWriter, SailError> {
458 let endpoint = self.exec_endpoint(sailbox_id).await?;
459 Ok(self.inner.worker.write_file(
460 &endpoint,
461 sailbox_id,
462 remote_path,
463 options.create_parents,
464 options.mode,
465 ))
466 }
467}