Skip to main content

boxxkite_client/
images.rs

1//! Declarative builder (custom sandbox images): `POST/GET/DELETE
2//! /v1/images*`. Mirrors `sdk-python`'s `create_image`/`get_image`/
3//! `list_images`/`delete_image`.
4
5use reqwest::Method;
6use serde::{Deserialize, Serialize};
7
8use crate::client::Client;
9use crate::error::BoxxkiteError;
10
11/// Pre-approved base image for [`Client::create_image`]. Each variant is
12/// itself a separate, boxxkite-maintained hardened image -- never a
13/// caller-supplied arbitrary base OS.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
15pub enum ImageBase {
16    /// The full data-science/document/browser stack.
17    #[serde(rename = "boxxkite-default")]
18    BoxxkiteDefault,
19    /// A lean Python+Node base with nothing preinstalled.
20    #[serde(rename = "boxxkite-minimal")]
21    BoxxkiteMinimal,
22    /// Drops Python entirely -- no `python_packages` installable.
23    #[serde(rename = "boxxkite-node")]
24    BoxxkiteNode,
25    /// Drops both Python and Node entirely -- no `python_packages` or
26    /// `npm_packages` installable.
27    #[serde(rename = "boxxkite-go")]
28    BoxxkiteGo,
29}
30
31/// Builder for `POST /v1/images`'s optional fields.
32#[derive(Debug, Clone, Default, Serialize)]
33pub struct CreateImageOptions {
34    #[serde(skip_serializing_if = "Option::is_none")]
35    label: Option<String>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    base: Option<ImageBase>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    python_packages: Option<Vec<String>>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    apt_packages: Option<Vec<String>>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    npm_packages: Option<Vec<String>>,
44}
45
46impl CreateImageOptions {
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    pub fn label(mut self, label: impl Into<String>) -> Self {
52        self.label = Some(label.into());
53        self
54    }
55
56    /// Defaults to [`ImageBase::BoxxkiteDefault`] server-side when omitted.
57    pub fn base(mut self, base: ImageBase) -> Self {
58        self.base = Some(base);
59        self
60    }
61
62    /// Exact-version-pinned packages (`"name==version"`, no ranges), e.g.
63    /// `"polars==1.9.0"`. Not supported on [`ImageBase::BoxxkiteNode`] or
64    /// [`ImageBase::BoxxkiteGo`].
65    pub fn python_packages<I, S>(mut self, packages: I) -> Self
66    where
67        I: IntoIterator<Item = S>,
68        S: Into<String>,
69    {
70        self.python_packages = Some(packages.into_iter().map(Into::into).collect());
71        self
72    }
73
74    /// Exact-version-pinned apt/apk packages (`"name==version"`, no ranges).
75    pub fn apt_packages<I, S>(mut self, packages: I) -> Self
76    where
77        I: IntoIterator<Item = S>,
78        S: Into<String>,
79    {
80        self.apt_packages = Some(packages.into_iter().map(Into::into).collect());
81        self
82    }
83
84    /// Exact-version-pinned npm packages (`"name==version"`, no ranges), e.g.
85    /// `"typescript==5.6.0"`. Not supported on [`ImageBase::BoxxkiteGo`].
86    pub fn npm_packages<I, S>(mut self, packages: I) -> Self
87    where
88        I: IntoIterator<Item = S>,
89        S: Into<String>,
90    {
91        self.npm_packages = Some(packages.into_iter().map(Into::into).collect());
92        self
93    }
94}
95
96/// A custom sandbox image (`SandboxImageOut`/`SandboxImageBuildAccepted`).
97/// [`Client::create_image`]'s response only ever populates `id`/`label`/
98/// `status`/`created_at` -- the rest fill in once you poll
99/// [`Client::get_image`] and the build reaches a terminal state.
100#[derive(Debug, Clone, Deserialize)]
101pub struct Image {
102    pub id: String,
103    pub label: Option<String>,
104    #[serde(default)]
105    pub base: String,
106    #[serde(default)]
107    pub python_packages: Vec<String>,
108    #[serde(default)]
109    pub apt_packages: Vec<String>,
110    #[serde(default)]
111    pub npm_packages: Vec<String>,
112    /// `"queued"`, `"building"`, `"scanning"`, `"completed"`, `"failed"`, or
113    /// `"rejected"`.
114    pub status: String,
115    pub digest: Option<String>,
116    pub registry_ref: Option<String>,
117    pub scan_result: Option<serde_json::Value>,
118    pub failure_reason: Option<String>,
119    pub created_at: String,
120    pub completed_at: Option<String>,
121}
122
123impl Client {
124    /// `POST /v1/images` -- build a custom sandbox image (a base image plus
125    /// exact-version-pinned Python/apt/npm packages). Returns immediately
126    /// with a pending record; poll [`Client::get_image`] for `status` to
127    /// reach a terminal state.
128    pub async fn create_image(&self, options: CreateImageOptions) -> Result<Image, BoxxkiteError> {
129        let builder = self.request(Method::POST, "/v1/images").json(&options);
130        self.send(builder).await
131    }
132
133    /// `GET /v1/images/{id}` -- a custom sandbox image's build status and details.
134    pub async fn get_image(&self, image_id: &str) -> Result<Image, BoxxkiteError> {
135        let builder = self.request(Method::GET, &format!("/v1/images/{image_id}"));
136        self.send(builder).await
137    }
138
139    /// `GET /v1/images` -- every custom sandbox image built for this account.
140    pub async fn list_images(&self) -> Result<Vec<Image>, BoxxkiteError> {
141        let builder = self.request(Method::GET, "/v1/images");
142        self.send_or_default(builder).await
143    }
144
145    /// `DELETE /v1/images/{id}` -- delete a custom sandbox image.
146    pub async fn delete_image(&self, image_id: &str) -> Result<(), BoxxkiteError> {
147        let builder = self.request(Method::DELETE, &format!("/v1/images/{image_id}"));
148        self.send_no_content(builder).await
149    }
150}