1use std::sync::Arc;
30
31use serde_json::Value;
32
33use crate::app::{self, App, AppOverview};
34use crate::config::Config;
35use crate::error::SailError;
36use crate::exec::{ExecOptions, ExecParams, ExecProcess};
37use crate::http::HttpCore;
38use crate::imagebuilder::ImageBuilder;
39use crate::sailbox::api::{SailboxApi, UpgradeOutcome};
40use crate::sailbox::types::{
41 CreateSailboxRequest, ListQuery, NfsVolume, SailboxCheckpoint, SailboxHandle, SailboxInfo,
42 SailboxPage,
43};
44use crate::worker::{FileReader, FileWriter, UploadOptions, WorkerProxy};
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}
74
75impl ClientBuilder {
76 pub fn new(api_key: impl Into<String>) -> ClientBuilder {
79 ClientBuilder {
80 api_key: Some(api_key.into()),
81 ..ClientBuilder::default()
82 }
83 }
84
85 pub fn mode(mut self, mode: impl Into<String>) -> ClientBuilder {
88 self.mode = Some(mode.into());
89 self
90 }
91
92 pub fn api_key(mut self, api_key: impl Into<String>) -> ClientBuilder {
94 self.api_key = Some(api_key.into());
95 self
96 }
97
98 pub fn api_url(mut self, api_url: impl Into<String>) -> ClientBuilder {
100 self.api_url = Some(api_url.into());
101 self
102 }
103
104 pub fn sailbox_api_url(mut self, url: impl Into<String>) -> ClientBuilder {
106 self.sailbox_api_url = Some(url.into());
107 self
108 }
109
110 pub fn imagebuilder_url(mut self, url: impl Into<String>) -> ClientBuilder {
112 self.imagebuilder_url = Some(url.into());
113 self
114 }
115
116 pub fn build(self) -> Result<Client, SailError> {
118 let api_key = self.api_key.unwrap_or_default();
119 let config = Config::resolve(
120 self.mode.as_deref(),
121 api_key,
122 self.api_url,
123 self.sailbox_api_url,
124 self.imagebuilder_url,
125 None,
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(
198 &self,
199 req: &CreateSailboxRequest,
200 ) -> Result<SailboxHandle, SailError> {
201 self.sailbox_api().create(req).await
202 }
203
204 pub async fn get_sailbox(&self, sailbox_id: &str) -> Result<SailboxInfo, SailError> {
206 self.sailbox_api().get(sailbox_id).await
207 }
208
209 pub async fn list_sailboxes(&self, query: &ListQuery) -> Result<SailboxPage, SailError> {
211 self.sailbox_api().list(query).await
212 }
213
214 pub async fn terminate_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
216 self.sailbox_api().terminate(sailbox_id).await
217 }
218
219 pub async fn pause_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
221 self.sailbox_api().pause(sailbox_id).await
222 }
223
224 pub async fn sleep_sailbox(&self, sailbox_id: &str) -> Result<(), SailError> {
226 self.sailbox_api().sleep(sailbox_id).await
227 }
228
229 pub async fn resume_sailbox(&self, sailbox_id: &str) -> Result<SailboxHandle, SailError> {
231 self.sailbox_api().resume(sailbox_id).await
232 }
233
234 pub async fn checkpoint_sailbox(
236 &self,
237 sailbox_id: &str,
238 ) -> Result<SailboxCheckpoint, SailError> {
239 self.sailbox_api().checkpoint(sailbox_id).await
240 }
241
242 pub async fn create_from_checkpoint(
244 &self,
245 checkpoint_id: &str,
246 name: Option<&str>,
247 timeout_seconds: Option<i64>,
248 ) -> Result<SailboxHandle, SailError> {
249 self.sailbox_api()
250 .from_checkpoint(checkpoint_id, name, timeout_seconds)
251 .await
252 }
253
254 pub async fn upgrade_sailbox(&self, sailbox_id: &str) -> Result<UpgradeOutcome, SailError> {
256 self.sailbox_api().upgrade(sailbox_id).await
257 }
258
259 pub async fn expose_listener(
261 &self,
262 sailbox_id: &str,
263 guest_port: u32,
264 protocol: &str,
265 allowlist: &[String],
266 ) -> Result<Value, SailError> {
267 self.sailbox_api()
268 .expose(sailbox_id, guest_port, protocol, allowlist)
269 .await
270 }
271
272 pub async fn unexpose_listener(
274 &self,
275 sailbox_id: &str,
276 guest_port: u32,
277 ) -> Result<(), SailError> {
278 self.sailbox_api().unexpose(sailbox_id, guest_port).await
279 }
280
281 pub async fn list_listeners(
283 &self,
284 sailbox_id: &str,
285 ) -> Result<Vec<crate::worker::Listener>, SailError> {
286 self.sailbox_api().list_listeners(sailbox_id).await
287 }
288
289 pub async fn get_listener(
292 &self,
293 sailbox_id: &str,
294 guest_port: u32,
295 ) -> Result<crate::worker::Listener, SailError> {
296 self.sailbox_api()
297 .get_listener(sailbox_id, guest_port)
298 .await
299 }
300
301 pub async fn ingress_auth_headers(
303 &self,
304 sailbox_id: &str,
305 ) -> Result<Vec<(String, String)>, SailError> {
306 self.sailbox_api().ingress_auth_headers(sailbox_id).await
307 }
308
309 pub async fn get_volume(
313 &self,
314 name: &str,
315 mint_if_missing: bool,
316 ) -> Result<NfsVolume, SailError> {
317 self.sailbox_api().get_volume(name, mint_if_missing).await
318 }
319
320 pub async fn list_volumes(
322 &self,
323 max_objects: Option<i64>,
324 ) -> Result<Vec<NfsVolume>, SailError> {
325 self.sailbox_api().list_volumes(max_objects).await
326 }
327
328 pub async fn delete_volume(
330 &self,
331 volume_id: &str,
332 allow_missing: bool,
333 ) -> Result<Option<NfsVolume>, SailError> {
334 self.sailbox_api()
335 .delete_volume(volume_id, allow_missing)
336 .await
337 }
338
339 pub async fn find_app(&self, name: &str, mint_if_missing: bool) -> Result<App, SailError> {
343 app::find_app(&self.inner.api_http, name, mint_if_missing).await
344 }
345
346 pub async fn list_apps(&self) -> Result<Vec<AppOverview>, SailError> {
348 app::list_apps(&self.inner.sailbox_http).await
349 }
350
351 pub(crate) async fn exec_endpoint(&self, sailbox_id: &str) -> Result<String, SailError> {
361 let handle = self.resume_sailbox(sailbox_id).await?;
362 if handle.exec_endpoint.is_empty() {
363 return Err(SailError::Internal {
364 message: format!("sailbox {sailbox_id} resumed without an exec endpoint"),
365 });
366 }
367 Ok(handle.exec_endpoint)
368 }
369
370 pub async fn exec(
381 &self,
382 sailbox_id: &str,
383 argv: Vec<String>,
384 options: ExecOptions,
385 ) -> Result<ExecProcess, SailError> {
386 let exec_endpoint = self.exec_endpoint(sailbox_id).await?;
387 let params = ExecParams {
388 sailbox_id: sailbox_id.to_string(),
389 exec_endpoint,
390 argv,
391 timeout_seconds: options.timeout_seconds,
392 idempotency_key: options.idempotency_key,
393 open_stdin: options.open_stdin,
394 pty: options.pty,
395 term: options.term,
396 cols: options.cols,
397 rows: options.rows,
398 retry_timeout: options.retry_timeout,
399 extra_metadata: Vec::new(),
400 };
401 ExecProcess::start(self.worker(), params).await
402 }
403
404 pub async fn download_file(
412 &self,
413 sailbox_id: &str,
414 remote_path: &str,
415 ) -> Result<FileReader, SailError> {
416 let endpoint = self.exec_endpoint(sailbox_id).await?;
417 Ok(self
418 .inner
419 .worker
420 .read_file(&endpoint, sailbox_id, remote_path))
421 }
422
423 pub async fn upload_file(
432 &self,
433 sailbox_id: &str,
434 remote_path: &str,
435 options: UploadOptions,
436 ) -> Result<FileWriter, SailError> {
437 let endpoint = self.exec_endpoint(sailbox_id).await?;
438 Ok(self.inner.worker.write_file(
439 &endpoint,
440 sailbox_id,
441 remote_path,
442 options.create_parents,
443 options.mode,
444 ))
445 }
446}