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 SailboxMetricsQuery, SailboxMetricsResponse, SailboxPage, SailboxSpendQuery,
43 SailboxSpendResponse, VolumeInfo,
44};
45use crate::worker::{FileReader, FileWriter, Listener, WorkerProxy, WriteOptions};
46
47#[derive(Clone)]
49pub struct Client {
50 inner: Arc<Inner>,
51}
52
53struct Inner {
54 config: Config,
55 sailbox_http: HttpCore,
57 api_http: HttpCore,
59 worker: Arc<WorkerProxy>,
62 imagebuilder: ImageBuilder,
63}
64
65#[derive(Debug, Default, Clone)]
68pub struct ClientBuilder {
69 mode: Option<String>,
70 api_key: Option<String>,
71 api_url: Option<String>,
72 sailbox_api_url: Option<String>,
73 imagebuilder_url: Option<String>,
74 ingress_url: Option<String>,
75}
76
77impl ClientBuilder {
78 pub fn new(api_key: impl Into<String>) -> ClientBuilder {
81 ClientBuilder {
82 api_key: Some(api_key.into()),
83 ..ClientBuilder::default()
84 }
85 }
86
87 #[doc(hidden)]
90 pub fn mode(mut self, mode: impl Into<String>) -> ClientBuilder {
91 self.mode = Some(mode.into());
92 self
93 }
94
95 pub fn api_url(mut self, api_url: impl Into<String>) -> ClientBuilder {
97 self.api_url = Some(api_url.into());
98 self
99 }
100
101 pub fn sailbox_api_url(mut self, url: impl Into<String>) -> ClientBuilder {
103 self.sailbox_api_url = Some(url.into());
104 self
105 }
106
107 pub fn imagebuilder_url(mut self, url: impl Into<String>) -> ClientBuilder {
109 self.imagebuilder_url = Some(url.into());
110 self
111 }
112
113 pub fn ingress_url(mut self, url: impl Into<String>) -> ClientBuilder {
116 self.ingress_url = Some(url.into());
117 self
118 }
119
120 pub fn build(self) -> Result<Client, SailError> {
122 let api_key = self.api_key.unwrap_or_default();
123 let config = Config::resolve(
124 self.mode.as_deref(),
125 api_key,
126 self.api_url,
127 self.sailbox_api_url,
128 self.imagebuilder_url,
129 self.ingress_url,
130 )?;
131 Client::from_config(config)
132 }
133}
134
135impl Client {
136 pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
138 ClientBuilder::new(api_key)
139 }
140
141 pub fn from_env() -> Result<Client, SailError> {
143 Client::from_config(Config::from_env()?)
144 }
145
146 pub fn from_config(config: Config) -> Result<Client, SailError> {
148 let sailbox_http = HttpCore::new(&config.sailbox_api_url, &config.api_key)?;
149 let api_http = HttpCore::new(&config.api_url, &config.api_key)?;
150 let worker = Arc::new(WorkerProxy::new(&config.api_key)?);
151 let imagebuilder = ImageBuilder::new(&config.imagebuilder_url, &config.api_key)?;
152 Ok(Client {
153 inner: Arc::new(Inner {
154 config,
155 sailbox_http,
156 api_http,
157 worker,
158 imagebuilder,
159 }),
160 })
161 }
162
163 pub fn config(&self) -> &Config {
165 &self.inner.config
166 }
167
168 #[doc(hidden)]
170 pub fn worker(&self) -> Arc<WorkerProxy> {
171 Arc::clone(&self.inner.worker)
172 }
173
174 #[doc(hidden)]
176 pub fn imagebuilder(&self) -> &ImageBuilder {
177 &self.inner.imagebuilder
178 }
179
180 #[doc(hidden)]
182 pub fn sailbox_http(&self) -> &HttpCore {
183 &self.inner.sailbox_http
184 }
185
186 #[doc(hidden)]
188 pub fn api_http(&self) -> &HttpCore {
189 &self.inner.api_http
190 }
191
192 fn sailbox_api(&self) -> SailboxApi<'_> {
193 SailboxApi::new(&self.inner.sailbox_http)
194 }
195
196 pub async fn create_sailbox(
208 &self,
209 req: &CreateSailboxRequest,
210 timeout: Option<Duration>,
211 ) -> Result<Sailbox, SailError> {
212 let bind = |handle: SailboxHandle| Sailbox::bind(self.clone(), handle);
213 if !req.ssh {
214 return self.sailbox_api().create(req, timeout).await.map(bind);
215 }
216 crate::sailbox::api::validate_ingress_ports(&req.ingress_ports)?;
221 self.org_ssh_ca_public_key().await?;
224 let mut req = req.clone();
229 let ssh_allowlist = req
230 .ingress_ports
231 .iter()
232 .find(|port| port.guest_port == 22)
233 .map(|port| port.allowlist.clone())
234 .unwrap_or_default();
235 req.ingress_ports.retain(|port| port.guest_port != 22);
236 let handle = self.sailbox_api().create(&req, timeout).await?;
237 let handle_id = handle.sailbox_id.clone();
238 if let Err(err) = self
240 .enable_ssh(
241 &handle_id,
242 &ssh_allowlist,
243 false,
244 Duration::ZERO,
245 )
246 .await
247 {
248 return Err(SailError::Creation {
251 message: format!(
252 "sailbox {handle_id} was created, but SSH setup failed: {err}. Fetch it by \
253 id to retry enable_ssh or terminate it."
254 ),
255 status: 0,
256 body: serde_json::Value::Null,
257 });
258 }
259 Ok(bind(handle))
260 }
261
262 #[doc(hidden)]
264 pub async fn get_sailbox(&self, sailbox_id: &str) -> Result<SailboxInfo, SailError> {
265 self.sailbox_api().get(sailbox_id).await
266 }
267
268 pub async fn list_sailboxes(
270 &self,
271 query: &ListSailboxesQuery,
272 ) -> Result<SailboxPage, SailError> {
273 self.sailbox_api().list(query).await
274 }
275
276 pub async fn sailbox_spend(
278 &self,
279 query: &SailboxSpendQuery,
280 ) -> Result<SailboxSpendResponse, SailError> {
281 self.sailbox_api().spend(query).await
282 }
283
284 pub async fn sailbox_metrics(
286 &self,
287 sailbox_id: &str,
288 query: &SailboxMetricsQuery,
289 ) -> Result<SailboxMetricsResponse, SailError> {
290 self.sailbox_api().metrics(sailbox_id, query).await
291 }
292
293 #[doc(hidden)]
295 pub async fn terminate_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
296 self.sailbox_api().terminate(sailbox_id).await
297 }
298
299 #[doc(hidden)]
301 pub async fn pause_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
302 self.sailbox_api().pause(sailbox_id).await
303 }
304
305 #[doc(hidden)]
307 pub async fn sleep_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
308 self.sailbox_api().sleep(sailbox_id).await
309 }
310
311 #[doc(hidden)]
313 pub async fn resume_sailbox(&self, sailbox_id: &str) -> Result<SailboxHandle, SailError> {
314 self.sailbox_api().resume(sailbox_id).await
315 }
316
317 #[doc(hidden)]
319 pub async fn checkpoint_sailbox(
320 &self,
321 sailbox_id: &str,
322 name: Option<&str>,
323 ttl_seconds: Option<i64>,
324 ) -> Result<SailboxCheckpoint, SailError> {
325 self.sailbox_api()
326 .checkpoint(sailbox_id, name, ttl_seconds)
327 .await
328 }
329
330 pub async fn create_from_checkpoint(
332 &self,
333 checkpoint_id: &str,
334 name: Option<&str>,
335 timeout: Option<Duration>,
336 ) -> Result<Sailbox, SailError> {
337 self.sailbox_api()
338 .from_checkpoint(checkpoint_id, name, timeout.map(|t| t.as_secs() as i64))
339 .await
340 .map(|handle| Sailbox::bind(self.clone(), handle))
341 }
342
343 #[doc(hidden)]
345 pub async fn upgrade_sailbox(&self, sailbox_id: &str) -> Result<UpgradeResult, SailError> {
346 self.sailbox_api().upgrade(sailbox_id).await
347 }
348
349 #[doc(hidden)]
351 pub async fn expose_listener(
352 &self,
353 sailbox_id: &str,
354 guest_port: u32,
355 protocol: crate::sailbox::types::IngressProtocol,
356 allowlist: &[String],
357 ) -> Result<Listener, SailError> {
358 let mut response = self
359 .sailbox_api()
360 .expose(sailbox_id, guest_port, protocol, allowlist)
361 .await?;
362 self.fill_listener_url(sailbox_id, &mut response);
363 Ok(response)
364 }
365
366 #[doc(hidden)]
368 pub async fn unexpose_listener(
369 &self,
370 sailbox_id: &str,
371 guest_port: u32,
372 ) -> Result<(), SailError> {
373 self.sailbox_api().unexpose(sailbox_id, guest_port).await
374 }
375
376 #[doc(hidden)]
378 pub async fn list_listeners(
379 &self,
380 sailbox_id: &str,
381 ) -> Result<Vec<crate::worker::Listener>, SailError> {
382 let mut listeners = self.sailbox_api().list_listeners(sailbox_id).await?;
383 for listener in &mut listeners {
384 self.fill_listener_url(sailbox_id, listener);
385 }
386 Ok(listeners)
387 }
388
389 #[doc(hidden)]
392 pub async fn get_listener(
393 &self,
394 sailbox_id: &str,
395 guest_port: u32,
396 ) -> Result<crate::worker::Listener, SailError> {
397 let mut listener = self
398 .sailbox_api()
399 .get_listener(sailbox_id, guest_port)
400 .await?;
401 self.fill_listener_url(sailbox_id, &mut listener);
402 Ok(listener)
403 }
404
405 fn fill_listener_url(&self, sailbox_id: &str, listener: &mut crate::worker::Listener) {
409 if listener.public_url.is_empty()
410 && listener.protocol != crate::sailbox::types::ListenerProtocol::Tcp
411 {
412 listener.public_url = crate::sailbox::listeners::synthesized_public_url(
413 self.config(),
414 sailbox_id,
415 listener.guest_port,
416 );
417 }
418 }
419
420 #[doc(hidden)]
422 pub async fn ingress_auth_headers(
423 &self,
424 sailbox_id: &str,
425 ) -> Result<Vec<(String, String)>, SailError> {
426 self.sailbox_api().ingress_auth_headers(sailbox_id).await
427 }
428
429 pub async fn org_ssh_ca_public_key(&self) -> Result<String, SailError> {
431 self.sailbox_api().org_ssh_ca_public_key().await
432 }
433
434 pub async fn issue_user_cert(
438 &self,
439 public_key: &str,
440 timeout: Option<Duration>,
441 ) -> Result<crate::sailbox::types::IssuedUserCert, SailError> {
442 self.sailbox_api()
443 .issue_user_cert(public_key, timeout.map(|t| t.as_secs_f64()))
444 .await
445 }
446
447 pub async fn get_volume(
451 &self,
452 name: &str,
453 mint_if_missing: bool,
454 ) -> Result<VolumeInfo, SailError> {
455 self.sailbox_api().get_volume(name, mint_if_missing).await
456 }
457
458 pub async fn list_volumes(
460 &self,
461 max_objects: Option<i64>,
462 ) -> Result<Vec<VolumeInfo>, SailError> {
463 self.sailbox_api().list_volumes(max_objects).await
464 }
465
466 pub async fn delete_volume(
468 &self,
469 volume_id: &str,
470 allow_missing: bool,
471 ) -> Result<Option<VolumeInfo>, SailError> {
472 self.sailbox_api()
473 .delete_volume(volume_id, allow_missing)
474 .await
475 }
476
477 pub async fn find_app(&self, name: &str, mint_if_missing: bool) -> Result<App, SailError> {
481 app::find_app(&self.inner.api_http, name, mint_if_missing).await
482 }
483
484 pub async fn list_apps(&self) -> Result<Vec<App>, SailError> {
486 app::list_apps(&self.inner.api_http).await
487 }
488
489 #[doc(hidden)]
499 pub async fn exec_endpoint(&self, sailbox_id: &str) -> Result<String, SailError> {
500 let handle = self.resume_sailbox(sailbox_id).await?;
501 if handle.exec_endpoint.is_empty() {
502 return Err(SailError::Internal {
503 message: format!("sailbox {sailbox_id} resumed without an exec endpoint"),
504 });
505 }
506 Ok(handle.exec_endpoint)
507 }
508
509 #[doc(hidden)]
512 pub async fn exec(
513 &self,
514 sailbox_id: &str,
515 argv: Vec<String>,
516 options: ExecOptions,
517 ) -> Result<ExecProcess, SailError> {
518 if argv.is_empty() {
519 return Err(SailError::InvalidArgument {
520 message: "command must be non-empty".to_string(),
521 });
522 }
523 if options.cwd.is_some() || options.background {
524 return Err(SailError::InvalidArgument {
525 message: "cwd and background require a shell command; use exec_shell".to_string(),
526 });
527 }
528 let exec_endpoint = self.exec_endpoint(sailbox_id).await?;
529 let params = ExecParams {
530 sailbox_id: sailbox_id.to_string(),
531 exec_endpoint,
532 argv,
533 timeout_seconds: options
536 .timeout
537 .map_or(0, |d| d.as_secs_f64().ceil().max(1.0) as u32),
538 idempotency_key: options.idempotency_key,
539 open_stdin: options.open_stdin || options.pty,
542 pty: options.pty,
543 term: options.term,
544 cols: options.cols,
545 rows: options.rows,
546 retry_timeout: options.retry_timeout.as_secs_f64(),
547 extra_metadata: Vec::new(),
548 };
549 ExecProcess::start(self.worker(), params).await
550 }
551
552 #[doc(hidden)]
556 pub async fn exec_shell(
557 &self,
558 sailbox_id: &str,
559 command: &str,
560 mut options: ExecOptions,
561 ) -> Result<ExecProcess, SailError> {
562 let argv = crate::exec::shell_argv(command, &options)?;
563 options.cwd = None;
566 options.background = false;
567 self.exec(sailbox_id, argv, options).await
568 }
569
570 #[doc(hidden)]
578 pub async fn read_stream(
579 &self,
580 sailbox_id: &str,
581 remote_path: &str,
582 ) -> Result<FileReader, SailError> {
583 let endpoint = self.exec_endpoint(sailbox_id).await?;
584 Ok(self
585 .inner
586 .worker
587 .read_file(&endpoint, sailbox_id, remote_path))
588 }
589
590 #[doc(hidden)]
594 pub async fn read_file(
595 &self,
596 sailbox_id: &str,
597 remote_path: &str,
598 ) -> Result<Vec<u8>, SailError> {
599 let reader = self.read_stream(sailbox_id, remote_path).await?;
600 let mut contents = Vec::new();
601 while let Some(chunk) = reader.next().await {
602 contents.extend_from_slice(&chunk?);
603 }
604 Ok(contents)
605 }
606
607 #[doc(hidden)]
616 pub async fn write_stream(
617 &self,
618 sailbox_id: &str,
619 remote_path: &str,
620 options: WriteOptions,
621 ) -> Result<FileWriter, SailError> {
622 let endpoint = self.exec_endpoint(sailbox_id).await?;
623 Ok(self.inner.worker.write_file(
624 &endpoint,
625 sailbox_id,
626 remote_path,
627 options.create_parents,
628 options.mode,
629 ))
630 }
631
632 #[doc(hidden)]
636 pub async fn write_file(
637 &self,
638 sailbox_id: &str,
639 remote_path: &str,
640 data: &[u8],
641 options: WriteOptions,
642 ) -> Result<(), SailError> {
643 let mut writer = self.write_stream(sailbox_id, remote_path, options).await?;
644 writer.write(data).await?;
645 writer.finish().await
646 }
647}