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 pub fn mode(mut self, mode: impl Into<String>) -> ClientBuilder {
89 self.mode = Some(mode.into());
90 self
91 }
92
93 pub fn api_url(mut self, api_url: impl Into<String>) -> ClientBuilder {
95 self.api_url = Some(api_url.into());
96 self
97 }
98
99 pub fn sailbox_api_url(mut self, url: impl Into<String>) -> ClientBuilder {
101 self.sailbox_api_url = Some(url.into());
102 self
103 }
104
105 pub fn imagebuilder_url(mut self, url: impl Into<String>) -> ClientBuilder {
107 self.imagebuilder_url = Some(url.into());
108 self
109 }
110
111 pub fn ingress_url(mut self, url: impl Into<String>) -> ClientBuilder {
114 self.ingress_url = Some(url.into());
115 self
116 }
117
118 pub fn build(self) -> Result<Client, SailError> {
120 let api_key = self.api_key.unwrap_or_default();
121 let config = Config::resolve(
122 self.mode.as_deref(),
123 api_key,
124 self.api_url,
125 self.sailbox_api_url,
126 self.imagebuilder_url,
127 self.ingress_url,
128 )?;
129 Client::from_config(config)
130 }
131}
132
133impl Client {
134 pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
136 ClientBuilder::new(api_key)
137 }
138
139 pub fn from_env() -> Result<Client, SailError> {
141 Client::from_config(Config::from_env()?)
142 }
143
144 pub fn from_config(config: Config) -> Result<Client, SailError> {
146 let sailbox_http = HttpCore::new(&config.sailbox_api_url, &config.api_key)?;
147 let api_http = HttpCore::new(&config.api_url, &config.api_key)?;
148 let worker = Arc::new(WorkerProxy::new(&config.api_key)?);
149 let imagebuilder = ImageBuilder::new(&config.imagebuilder_url, &config.api_key)?;
150 Ok(Client {
151 inner: Arc::new(Inner {
152 config,
153 sailbox_http,
154 api_http,
155 worker,
156 imagebuilder,
157 }),
158 })
159 }
160
161 pub fn config(&self) -> &Config {
163 &self.inner.config
164 }
165
166 #[doc(hidden)]
168 pub fn worker(&self) -> Arc<WorkerProxy> {
169 Arc::clone(&self.inner.worker)
170 }
171
172 #[doc(hidden)]
174 pub fn imagebuilder(&self) -> &ImageBuilder {
175 &self.inner.imagebuilder
176 }
177
178 #[doc(hidden)]
180 pub fn sailbox_http(&self) -> &HttpCore {
181 &self.inner.sailbox_http
182 }
183
184 #[doc(hidden)]
186 pub fn api_http(&self) -> &HttpCore {
187 &self.inner.api_http
188 }
189
190 fn sailbox_api(&self) -> SailboxApi<'_> {
191 SailboxApi::new(&self.inner.sailbox_http)
192 }
193
194 pub async fn create_sailbox(
206 &self,
207 req: &CreateSailboxRequest,
208 timeout: Option<Duration>,
209 ) -> Result<Sailbox, SailError> {
210 let bind = |handle: SailboxHandle| Sailbox::bind(self.clone(), handle);
211 if !req.ssh {
212 return self.sailbox_api().create(req, timeout).await.map(bind);
213 }
214 crate::sailbox::api::validate_ingress_ports(&req.ingress_ports)?;
219 self.org_ssh_ca_public_key().await?;
222 let mut req = req.clone();
227 let ssh_allowlist = req
228 .ingress_ports
229 .iter()
230 .find(|port| port.guest_port == 22)
231 .map(|port| port.allowlist.clone())
232 .unwrap_or_default();
233 req.ingress_ports.retain(|port| port.guest_port != 22);
234 let handle = self.sailbox_api().create(&req, timeout).await?;
235 let handle_id = handle.sailbox_id.clone();
236 if let Err(err) = self
238 .enable_ssh(&handle_id, &ssh_allowlist, false, Duration::ZERO)
239 .await
240 {
241 return Err(SailError::Creation {
244 message: format!(
245 "sailbox {handle_id} was created, but SSH setup failed: {err}. Fetch it by \
246 id to retry enable_ssh or terminate it."
247 ),
248 status: 0,
249 body: serde_json::Value::Null,
250 });
251 }
252 Ok(bind(handle))
253 }
254
255 #[doc(hidden)]
257 pub async fn get_sailbox(&self, sailbox_id: &str) -> Result<SailboxInfo, SailError> {
258 self.sailbox_api().get(sailbox_id).await
259 }
260
261 pub async fn list_sailboxes(
263 &self,
264 query: &ListSailboxesQuery,
265 ) -> Result<SailboxPage, SailError> {
266 self.sailbox_api().list(query).await
267 }
268
269 #[doc(hidden)]
271 pub async fn terminate_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
272 self.sailbox_api().terminate(sailbox_id).await
273 }
274
275 #[doc(hidden)]
277 pub async fn pause_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
278 self.sailbox_api().pause(sailbox_id).await
279 }
280
281 #[doc(hidden)]
283 pub async fn sleep_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
284 self.sailbox_api().sleep(sailbox_id).await
285 }
286
287 #[doc(hidden)]
289 pub async fn resume_sailbox(&self, sailbox_id: &str) -> Result<SailboxHandle, SailError> {
290 self.sailbox_api().resume(sailbox_id).await
291 }
292
293 #[doc(hidden)]
295 pub async fn checkpoint_sailbox(
296 &self,
297 sailbox_id: &str,
298 name: Option<&str>,
299 ttl_seconds: Option<i64>,
300 ) -> Result<SailboxCheckpoint, SailError> {
301 self.sailbox_api()
302 .checkpoint(sailbox_id, name, ttl_seconds)
303 .await
304 }
305
306 pub async fn create_from_checkpoint(
308 &self,
309 checkpoint_id: &str,
310 name: Option<&str>,
311 timeout: Option<Duration>,
312 ) -> Result<Sailbox, SailError> {
313 self.sailbox_api()
314 .from_checkpoint(checkpoint_id, name, timeout.map(|t| t.as_secs() as i64))
315 .await
316 .map(|handle| Sailbox::bind(self.clone(), handle))
317 }
318
319 #[doc(hidden)]
321 pub async fn upgrade_sailbox(&self, sailbox_id: &str) -> Result<UpgradeResult, SailError> {
322 self.sailbox_api().upgrade(sailbox_id).await
323 }
324
325 #[doc(hidden)]
327 pub async fn expose_listener(
328 &self,
329 sailbox_id: &str,
330 guest_port: u32,
331 protocol: crate::sailbox::types::IngressProtocol,
332 allowlist: &[String],
333 ) -> Result<Listener, SailError> {
334 let mut response = self
335 .sailbox_api()
336 .expose(sailbox_id, guest_port, protocol, allowlist)
337 .await?;
338 self.fill_listener_url(sailbox_id, &mut response);
339 Ok(response)
340 }
341
342 #[doc(hidden)]
344 pub async fn unexpose_listener(
345 &self,
346 sailbox_id: &str,
347 guest_port: u32,
348 ) -> Result<(), SailError> {
349 self.sailbox_api().unexpose(sailbox_id, guest_port).await
350 }
351
352 #[doc(hidden)]
354 pub async fn list_listeners(
355 &self,
356 sailbox_id: &str,
357 ) -> Result<Vec<crate::worker::Listener>, SailError> {
358 let mut listeners = self.sailbox_api().list_listeners(sailbox_id).await?;
359 for listener in &mut listeners {
360 self.fill_listener_url(sailbox_id, listener);
361 }
362 Ok(listeners)
363 }
364
365 #[doc(hidden)]
368 pub async fn get_listener(
369 &self,
370 sailbox_id: &str,
371 guest_port: u32,
372 ) -> Result<crate::worker::Listener, SailError> {
373 let mut listener = self
374 .sailbox_api()
375 .get_listener(sailbox_id, guest_port)
376 .await?;
377 self.fill_listener_url(sailbox_id, &mut listener);
378 Ok(listener)
379 }
380
381 fn fill_listener_url(&self, sailbox_id: &str, listener: &mut crate::worker::Listener) {
385 if listener.public_url.is_empty()
386 && listener.protocol != crate::sailbox::types::ListenerProtocol::Tcp
387 {
388 listener.public_url = crate::sailbox::listeners::synthesized_public_url(
389 self.config(),
390 sailbox_id,
391 listener.guest_port,
392 );
393 }
394 }
395
396 #[doc(hidden)]
398 pub async fn ingress_auth_headers(
399 &self,
400 sailbox_id: &str,
401 ) -> Result<Vec<(String, String)>, SailError> {
402 self.sailbox_api().ingress_auth_headers(sailbox_id).await
403 }
404
405 pub async fn org_ssh_ca_public_key(&self) -> Result<String, SailError> {
407 self.sailbox_api().org_ssh_ca_public_key().await
408 }
409
410 pub async fn issue_user_cert(
414 &self,
415 public_key: &str,
416 timeout: Option<Duration>,
417 ) -> Result<crate::sailbox::types::IssuedUserCert, SailError> {
418 self.sailbox_api()
419 .issue_user_cert(public_key, timeout.map(|t| t.as_secs_f64()))
420 .await
421 }
422
423 pub async fn get_volume(
427 &self,
428 name: &str,
429 mint_if_missing: bool,
430 ) -> Result<VolumeInfo, SailError> {
431 self.sailbox_api().get_volume(name, mint_if_missing).await
432 }
433
434 pub async fn list_volumes(
436 &self,
437 max_objects: Option<i64>,
438 ) -> Result<Vec<VolumeInfo>, SailError> {
439 self.sailbox_api().list_volumes(max_objects).await
440 }
441
442 pub async fn delete_volume(
444 &self,
445 volume_id: &str,
446 allow_missing: bool,
447 ) -> Result<Option<VolumeInfo>, SailError> {
448 self.sailbox_api()
449 .delete_volume(volume_id, allow_missing)
450 .await
451 }
452
453 pub async fn find_app(&self, name: &str, mint_if_missing: bool) -> Result<App, SailError> {
457 app::find_app(&self.inner.api_http, name, mint_if_missing).await
458 }
459
460 pub async fn list_apps(&self) -> Result<Vec<App>, SailError> {
462 app::list_apps(&self.inner.api_http).await
463 }
464
465 #[doc(hidden)]
475 pub async fn exec_endpoint(&self, sailbox_id: &str) -> Result<String, SailError> {
476 let handle = self.resume_sailbox(sailbox_id).await?;
477 if handle.exec_endpoint.is_empty() {
478 return Err(SailError::Internal {
479 message: format!("sailbox {sailbox_id} resumed without an exec endpoint"),
480 });
481 }
482 Ok(handle.exec_endpoint)
483 }
484
485 #[doc(hidden)]
488 pub async fn exec(
489 &self,
490 sailbox_id: &str,
491 argv: Vec<String>,
492 options: ExecOptions,
493 ) -> Result<ExecProcess, SailError> {
494 if argv.is_empty() {
495 return Err(SailError::InvalidArgument {
496 message: "command must be non-empty".to_string(),
497 });
498 }
499 if options.cwd.is_some() || options.background {
500 return Err(SailError::InvalidArgument {
501 message: "cwd and background require a shell command; use exec_shell".to_string(),
502 });
503 }
504 let exec_endpoint = self.exec_endpoint(sailbox_id).await?;
505 let params = ExecParams {
506 sailbox_id: sailbox_id.to_string(),
507 exec_endpoint,
508 argv,
509 timeout_seconds: options
512 .timeout
513 .map_or(0, |d| d.as_secs_f64().ceil().max(1.0) as u32),
514 idempotency_key: options.idempotency_key,
515 open_stdin: options.open_stdin || options.pty,
518 pty: options.pty,
519 term: options.term,
520 cols: options.cols,
521 rows: options.rows,
522 retry_timeout: options.retry_timeout.as_secs_f64(),
523 extra_metadata: Vec::new(),
524 };
525 ExecProcess::start(self.worker(), params).await
526 }
527
528 #[doc(hidden)]
532 pub async fn exec_shell(
533 &self,
534 sailbox_id: &str,
535 command: &str,
536 mut options: ExecOptions,
537 ) -> Result<ExecProcess, SailError> {
538 let argv = crate::exec::shell_argv(command, &options)?;
539 options.cwd = None;
542 options.background = false;
543 self.exec(sailbox_id, argv, options).await
544 }
545
546 #[doc(hidden)]
554 pub async fn read_stream(
555 &self,
556 sailbox_id: &str,
557 remote_path: &str,
558 ) -> Result<FileReader, SailError> {
559 let endpoint = self.exec_endpoint(sailbox_id).await?;
560 Ok(self
561 .inner
562 .worker
563 .read_file(&endpoint, sailbox_id, remote_path))
564 }
565
566 #[doc(hidden)]
570 pub async fn read_file(
571 &self,
572 sailbox_id: &str,
573 remote_path: &str,
574 ) -> Result<Vec<u8>, SailError> {
575 let reader = self.read_stream(sailbox_id, remote_path).await?;
576 let mut contents = Vec::new();
577 while let Some(chunk) = reader.next().await {
578 contents.extend_from_slice(&chunk?);
579 }
580 Ok(contents)
581 }
582
583 #[doc(hidden)]
592 pub async fn write_stream(
593 &self,
594 sailbox_id: &str,
595 remote_path: &str,
596 options: WriteOptions,
597 ) -> Result<FileWriter, SailError> {
598 let endpoint = self.exec_endpoint(sailbox_id).await?;
599 Ok(self.inner.worker.write_file(
600 &endpoint,
601 sailbox_id,
602 remote_path,
603 options.create_parents,
604 options.mode,
605 ))
606 }
607
608 #[doc(hidden)]
612 pub async fn write_file(
613 &self,
614 sailbox_id: &str,
615 remote_path: &str,
616 data: &[u8],
617 options: WriteOptions,
618 ) -> Result<(), SailError> {
619 let mut writer = self.write_stream(sailbox_id, remote_path, options).await?;
620 writer.write(data).await?;
621 writer.finish().await
622 }
623}