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, UpgradeResult};
39use crate::sailbox::object::Sailbox;
40use crate::sailbox::types::{
41 CreateSailboxRequest, ListSailboxesQuery, SailboxCheckpoint, SailboxHandle, SailboxInfo,
42 SailboxPage, VolumeInfo,
43};
44use crate::worker::{FileReader, FileWriter, Listener, WorkerProxy, WriteOptions};
45
46#[derive(Clone)]
48pub struct Client {
49 inner: Arc<Inner>,
50}
51
52struct Inner {
53 config: Config,
54 sailbox_http: HttpCore,
56 api_http: HttpCore,
58 worker: Arc<WorkerProxy>,
61 imagebuilder: ImageBuilder,
62}
63
64#[derive(Debug, Default, Clone)]
67pub struct ClientBuilder {
68 mode: Option<String>,
69 api_key: Option<String>,
70 api_url: Option<String>,
71 sailbox_api_url: Option<String>,
72 imagebuilder_url: Option<String>,
73 ingress_url: Option<String>,
74}
75
76impl ClientBuilder {
77 pub fn new(api_key: impl Into<String>) -> ClientBuilder {
80 ClientBuilder {
81 api_key: Some(api_key.into()),
82 ..ClientBuilder::default()
83 }
84 }
85
86 #[doc(hidden)]
89 pub fn mode(mut self, mode: impl Into<String>) -> ClientBuilder {
90 self.mode = Some(mode.into());
91 self
92 }
93
94 pub fn api_url(mut self, api_url: impl Into<String>) -> ClientBuilder {
96 self.api_url = Some(api_url.into());
97 self
98 }
99
100 pub fn sailbox_api_url(mut self, url: impl Into<String>) -> ClientBuilder {
102 self.sailbox_api_url = Some(url.into());
103 self
104 }
105
106 pub fn imagebuilder_url(mut self, url: impl Into<String>) -> ClientBuilder {
108 self.imagebuilder_url = Some(url.into());
109 self
110 }
111
112 pub fn ingress_url(mut self, url: impl Into<String>) -> ClientBuilder {
115 self.ingress_url = Some(url.into());
116 self
117 }
118
119 pub fn build(self) -> Result<Client, SailError> {
121 let api_key = self.api_key.unwrap_or_default();
122 let config = Config::resolve(
123 self.mode.as_deref(),
124 api_key,
125 self.api_url,
126 self.sailbox_api_url,
127 self.imagebuilder_url,
128 self.ingress_url,
129 )?;
130 Client::from_config(config)
131 }
132}
133
134impl Client {
135 pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
137 ClientBuilder::new(api_key)
138 }
139
140 pub fn from_env() -> Result<Client, SailError> {
142 Client::from_config(Config::from_env()?)
143 }
144
145 pub fn from_config(config: Config) -> Result<Client, SailError> {
147 let sailbox_http = HttpCore::new(&config.sailbox_api_url, &config.api_key)?;
148 let api_http = HttpCore::new(&config.api_url, &config.api_key)?;
149 let worker = Arc::new(WorkerProxy::new(&config.api_key)?);
150 let imagebuilder = ImageBuilder::new(&config.imagebuilder_url, &config.api_key)?;
151 Ok(Client {
152 inner: Arc::new(Inner {
153 config,
154 sailbox_http,
155 api_http,
156 worker,
157 imagebuilder,
158 }),
159 })
160 }
161
162 pub fn config(&self) -> &Config {
164 &self.inner.config
165 }
166
167 #[doc(hidden)]
169 pub fn worker(&self) -> Arc<WorkerProxy> {
170 Arc::clone(&self.inner.worker)
171 }
172
173 #[doc(hidden)]
175 pub fn imagebuilder(&self) -> &ImageBuilder {
176 &self.inner.imagebuilder
177 }
178
179 #[doc(hidden)]
181 pub fn sailbox_http(&self) -> &HttpCore {
182 &self.inner.sailbox_http
183 }
184
185 #[doc(hidden)]
187 pub fn api_http(&self) -> &HttpCore {
188 &self.inner.api_http
189 }
190
191 fn sailbox_api(&self) -> SailboxApi<'_> {
192 SailboxApi::new(&self.inner.sailbox_http)
193 }
194
195 pub async fn create_sailbox(
207 &self,
208 req: &CreateSailboxRequest,
209 timeout: Option<Duration>,
210 ) -> Result<Sailbox, SailError> {
211 let bind = |handle: SailboxHandle| Sailbox::bind(self.clone(), handle);
212 if !req.ssh {
213 return self.sailbox_api().create(req, timeout).await.map(bind);
214 }
215 crate::sailbox::api::validate_ingress_ports(&req.ingress_ports)?;
220 self.org_ssh_ca_public_key().await?;
223 let mut req = req.clone();
228 let ssh_allowlist = req
229 .ingress_ports
230 .iter()
231 .find(|port| port.guest_port == 22)
232 .map(|port| port.allowlist.clone())
233 .unwrap_or_default();
234 req.ingress_ports.retain(|port| port.guest_port != 22);
235 let handle = self.sailbox_api().create(&req, timeout).await?;
236 let handle_id = handle.sailbox_id.clone();
237 if let Err(err) = self
239 .enable_ssh(
240 &handle_id,
241 &ssh_allowlist,
242 false,
243 Duration::ZERO,
244 )
245 .await
246 {
247 return Err(SailError::Creation {
250 message: format!(
251 "sailbox {handle_id} was created, but SSH setup failed: {err}. Fetch it by \
252 id to retry enable_ssh or terminate it."
253 ),
254 status: 0,
255 body: serde_json::Value::Null,
256 });
257 }
258 Ok(bind(handle))
259 }
260
261 #[doc(hidden)]
263 pub async fn get_sailbox(&self, sailbox_id: &str) -> Result<SailboxInfo, SailError> {
264 self.sailbox_api().get(sailbox_id).await
265 }
266
267 pub async fn list_sailboxes(
269 &self,
270 query: &ListSailboxesQuery,
271 ) -> Result<SailboxPage, SailError> {
272 self.sailbox_api().list(query).await
273 }
274
275 #[doc(hidden)]
277 pub async fn terminate_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
278 self.sailbox_api().terminate(sailbox_id).await
279 }
280
281 #[doc(hidden)]
283 pub async fn pause_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
284 self.sailbox_api().pause(sailbox_id).await
285 }
286
287 #[doc(hidden)]
289 pub async fn sleep_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
290 self.sailbox_api().sleep(sailbox_id).await
291 }
292
293 #[doc(hidden)]
295 pub async fn resume_sailbox(&self, sailbox_id: &str) -> Result<SailboxHandle, SailError> {
296 self.sailbox_api().resume(sailbox_id).await
297 }
298
299 #[doc(hidden)]
301 pub async fn checkpoint_sailbox(
302 &self,
303 sailbox_id: &str,
304 name: Option<&str>,
305 ttl_seconds: Option<i64>,
306 ) -> Result<SailboxCheckpoint, SailError> {
307 self.sailbox_api()
308 .checkpoint(sailbox_id, name, ttl_seconds)
309 .await
310 }
311
312 pub async fn create_from_checkpoint(
314 &self,
315 checkpoint_id: &str,
316 name: Option<&str>,
317 timeout: Option<Duration>,
318 ) -> Result<Sailbox, SailError> {
319 self.sailbox_api()
320 .from_checkpoint(checkpoint_id, name, timeout.map(|t| t.as_secs() as i64))
321 .await
322 .map(|handle| Sailbox::bind(self.clone(), handle))
323 }
324
325 #[doc(hidden)]
327 pub async fn upgrade_sailbox(&self, sailbox_id: &str) -> Result<UpgradeResult, SailError> {
328 self.sailbox_api().upgrade(sailbox_id).await
329 }
330
331 #[doc(hidden)]
333 pub async fn expose_listener(
334 &self,
335 sailbox_id: &str,
336 guest_port: u32,
337 protocol: crate::sailbox::types::IngressProtocol,
338 allowlist: &[String],
339 ) -> Result<Listener, SailError> {
340 let mut response = self
341 .sailbox_api()
342 .expose(sailbox_id, guest_port, protocol, allowlist)
343 .await?;
344 self.fill_listener_url(sailbox_id, &mut response);
345 Ok(response)
346 }
347
348 #[doc(hidden)]
350 pub async fn unexpose_listener(
351 &self,
352 sailbox_id: &str,
353 guest_port: u32,
354 ) -> Result<(), SailError> {
355 self.sailbox_api().unexpose(sailbox_id, guest_port).await
356 }
357
358 #[doc(hidden)]
360 pub async fn list_listeners(
361 &self,
362 sailbox_id: &str,
363 ) -> Result<Vec<crate::worker::Listener>, SailError> {
364 let mut listeners = self.sailbox_api().list_listeners(sailbox_id).await?;
365 for listener in &mut listeners {
366 self.fill_listener_url(sailbox_id, listener);
367 }
368 Ok(listeners)
369 }
370
371 #[doc(hidden)]
374 pub async fn get_listener(
375 &self,
376 sailbox_id: &str,
377 guest_port: u32,
378 ) -> Result<crate::worker::Listener, SailError> {
379 let mut listener = self
380 .sailbox_api()
381 .get_listener(sailbox_id, guest_port)
382 .await?;
383 self.fill_listener_url(sailbox_id, &mut listener);
384 Ok(listener)
385 }
386
387 fn fill_listener_url(&self, sailbox_id: &str, listener: &mut crate::worker::Listener) {
391 if listener.public_url.is_empty()
392 && listener.protocol != crate::sailbox::types::ListenerProtocol::Tcp
393 {
394 listener.public_url = crate::sailbox::listeners::synthesized_public_url(
395 self.config(),
396 sailbox_id,
397 listener.guest_port,
398 );
399 }
400 }
401
402 #[doc(hidden)]
404 pub async fn ingress_auth_headers(
405 &self,
406 sailbox_id: &str,
407 ) -> Result<Vec<(String, String)>, SailError> {
408 self.sailbox_api().ingress_auth_headers(sailbox_id).await
409 }
410
411 pub async fn org_ssh_ca_public_key(&self) -> Result<String, SailError> {
413 self.sailbox_api().org_ssh_ca_public_key().await
414 }
415
416 pub async fn issue_user_cert(
420 &self,
421 public_key: &str,
422 timeout: Option<Duration>,
423 ) -> Result<crate::sailbox::types::IssuedUserCert, SailError> {
424 self.sailbox_api()
425 .issue_user_cert(public_key, timeout.map(|t| t.as_secs_f64()))
426 .await
427 }
428
429 pub async fn get_volume(
433 &self,
434 name: &str,
435 mint_if_missing: bool,
436 ) -> Result<VolumeInfo, SailError> {
437 self.sailbox_api().get_volume(name, mint_if_missing).await
438 }
439
440 pub async fn list_volumes(
442 &self,
443 max_objects: Option<i64>,
444 ) -> Result<Vec<VolumeInfo>, SailError> {
445 self.sailbox_api().list_volumes(max_objects).await
446 }
447
448 pub async fn delete_volume(
450 &self,
451 volume_id: &str,
452 allow_missing: bool,
453 ) -> Result<Option<VolumeInfo>, SailError> {
454 self.sailbox_api()
455 .delete_volume(volume_id, allow_missing)
456 .await
457 }
458
459 pub async fn find_app(&self, name: &str, mint_if_missing: bool) -> Result<App, SailError> {
463 app::find_app(&self.inner.api_http, name, mint_if_missing).await
464 }
465
466 pub async fn list_apps(&self) -> Result<Vec<App>, SailError> {
468 app::list_apps(&self.inner.api_http).await
469 }
470
471 #[doc(hidden)]
481 pub async fn exec_endpoint(&self, sailbox_id: &str) -> Result<String, SailError> {
482 let handle = self.resume_sailbox(sailbox_id).await?;
483 if handle.exec_endpoint.is_empty() {
484 return Err(SailError::Internal {
485 message: format!("sailbox {sailbox_id} resumed without an exec endpoint"),
486 });
487 }
488 Ok(handle.exec_endpoint)
489 }
490
491 #[doc(hidden)]
494 pub async fn exec(
495 &self,
496 sailbox_id: &str,
497 argv: Vec<String>,
498 options: ExecOptions,
499 ) -> Result<ExecProcess, SailError> {
500 if argv.is_empty() {
501 return Err(SailError::InvalidArgument {
502 message: "command must be non-empty".to_string(),
503 });
504 }
505 if options.cwd.is_some() || options.background {
506 return Err(SailError::InvalidArgument {
507 message: "cwd and background require a shell command; use exec_shell".to_string(),
508 });
509 }
510 let exec_endpoint = self.exec_endpoint(sailbox_id).await?;
511 let params = ExecParams {
512 sailbox_id: sailbox_id.to_string(),
513 exec_endpoint,
514 argv,
515 timeout_seconds: options
518 .timeout
519 .map_or(0, |d| d.as_secs_f64().ceil().max(1.0) as u32),
520 idempotency_key: options.idempotency_key,
521 open_stdin: options.open_stdin || options.pty,
524 pty: options.pty,
525 term: options.term,
526 cols: options.cols,
527 rows: options.rows,
528 retry_timeout: options.retry_timeout.as_secs_f64(),
529 extra_metadata: Vec::new(),
530 };
531 ExecProcess::start(self.worker(), params).await
532 }
533
534 #[doc(hidden)]
538 pub async fn exec_shell(
539 &self,
540 sailbox_id: &str,
541 command: &str,
542 mut options: ExecOptions,
543 ) -> Result<ExecProcess, SailError> {
544 let argv = crate::exec::shell_argv(command, &options)?;
545 options.cwd = None;
548 options.background = false;
549 self.exec(sailbox_id, argv, options).await
550 }
551
552 #[doc(hidden)]
560 pub async fn read_stream(
561 &self,
562 sailbox_id: &str,
563 remote_path: &str,
564 ) -> Result<FileReader, SailError> {
565 let endpoint = self.exec_endpoint(sailbox_id).await?;
566 Ok(self
567 .inner
568 .worker
569 .read_file(&endpoint, sailbox_id, remote_path))
570 }
571
572 #[doc(hidden)]
576 pub async fn read_file(
577 &self,
578 sailbox_id: &str,
579 remote_path: &str,
580 ) -> Result<Vec<u8>, SailError> {
581 let reader = self.read_stream(sailbox_id, remote_path).await?;
582 let mut contents = Vec::new();
583 while let Some(chunk) = reader.next().await {
584 contents.extend_from_slice(&chunk?);
585 }
586 Ok(contents)
587 }
588
589 #[doc(hidden)]
598 pub async fn write_stream(
599 &self,
600 sailbox_id: &str,
601 remote_path: &str,
602 options: WriteOptions,
603 ) -> Result<FileWriter, SailError> {
604 let endpoint = self.exec_endpoint(sailbox_id).await?;
605 Ok(self.inner.worker.write_file(
606 &endpoint,
607 sailbox_id,
608 remote_path,
609 options.create_parents,
610 options.mode,
611 ))
612 }
613
614 #[doc(hidden)]
618 pub async fn write_file(
619 &self,
620 sailbox_id: &str,
621 remote_path: &str,
622 data: &[u8],
623 options: WriteOptions,
624 ) -> Result<(), SailError> {
625 let mut writer = self.write_stream(sailbox_id, remote_path, options).await?;
626 writer.write(data).await?;
627 writer.finish().await
628 }
629}