Skip to main content

a3s_box_sdk/client/
types.rs

1/// Result type used by the direct SDK client.
2pub type Result<T> = std::result::Result<T, ClientError>;
3
4/// Errors returned by the direct SDK client.
5#[derive(Debug, thiserror::Error)]
6pub enum ClientError {
7    #[error("state error: {0}")]
8    State(#[from] std::io::Error),
9    #[error("runtime error: {0}")]
10    Runtime(#[from] a3s_box_core::error::BoxError),
11    #[error("execution lifecycle error: {0}")]
12    Execution(#[from] a3s_box_core::ExecutionManagerError),
13    #[error("validation error: {0}")]
14    Validation(String),
15    #[error("guest operation failed: {0}")]
16    Guest(String),
17    #[error("box not found: {0}")]
18    BoxNotFound(String),
19    #[error("box query {query:?} matched multiple boxes: {matches:?}")]
20    AmbiguousBoxQuery { query: String, matches: Vec<String> },
21}
22
23/// Filesystem locations used by [`A3sBoxClient`].
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct A3sBoxPaths {
26    pub home: PathBuf,
27    pub boxes_file: PathBuf,
28    pub images_dir: PathBuf,
29    pub volumes_file: PathBuf,
30    pub volumes_dir: PathBuf,
31    pub networks_file: PathBuf,
32    pub snapshots_dir: PathBuf,
33}
34
35impl A3sBoxPaths {
36    /// Build paths under an a3s-box home directory.
37    pub fn from_home(home: impl Into<PathBuf>) -> Self {
38        let home = home.into();
39        Self {
40            boxes_file: home.join("boxes.json"),
41            images_dir: home.join("images"),
42            volumes_file: home.join("volumes.json"),
43            volumes_dir: home.join("volumes"),
44            networks_file: home.join("networks.json"),
45            snapshots_dir: home.join("snapshots"),
46            home,
47        }
48    }
49}
50
51impl Default for A3sBoxPaths {
52    fn default() -> Self {
53        Self::from_home(a3s_box_core::dirs_home())
54    }
55}
56
57/// Options for listing boxes.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct ListBoxesOptions {
60    /// Include stopped, dead, and created boxes.
61    pub all: bool,
62}
63
64impl ListBoxesOptions {
65    pub const fn all() -> Self {
66        Self { all: true }
67    }
68
69    pub const fn active() -> Self {
70        Self { all: false }
71    }
72}
73
74impl Default for ListBoxesOptions {
75    fn default() -> Self {
76        Self::all()
77    }
78}
79
80/// Options for reading a bounded box log snapshot.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub struct ReadBoxLogsOptions {
83    /// Number of lines to return from the end of the log source.
84    pub tail: usize,
85}
86
87impl ReadBoxLogsOptions {
88    pub const fn tail(tail: usize) -> Self {
89        Self { tail }
90    }
91}
92
93impl Default for ReadBoxLogsOptions {
94    fn default() -> Self {
95        Self { tail: 100 }
96    }
97}
98
99/// Optional registry credentials for pull and push operations.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct RegistryCredentials {
102    pub username: String,
103    pub password: String,
104}
105
106impl RegistryCredentials {
107    pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
108        Self {
109            username: username.into(),
110            password: password.into(),
111        }
112    }
113
114    fn into_auth(self) -> RegistryAuth {
115        RegistryAuth::basic(self.username, self.password)
116    }
117
118    fn validate(&self) -> Result<()> {
119        if self.username.trim().is_empty() {
120            return Err(ClientError::Validation(
121                "registry username cannot be empty".to_string(),
122            ));
123        }
124        if self.password.is_empty() {
125            return Err(ClientError::Validation(
126                "registry password cannot be empty".to_string(),
127            ));
128        }
129        Ok(())
130    }
131}
132
133/// Request to pull an OCI image through the runtime image puller.
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct PullImage {
136    pub reference: String,
137    pub force: bool,
138    pub platform: Option<String>,
139    pub signature_policy: SignaturePolicy,
140    pub credentials: Option<RegistryCredentials>,
141}
142
143impl PullImage {
144    pub fn new(reference: impl Into<String>) -> Self {
145        Self {
146            reference: reference.into(),
147            force: false,
148            platform: None,
149            signature_policy: SignaturePolicy::default(),
150            credentials: None,
151        }
152    }
153
154    pub fn force(mut self, force: bool) -> Self {
155        self.force = force;
156        self
157    }
158
159    pub fn platform(mut self, platform: impl Into<String>) -> Self {
160        self.platform = Some(platform.into());
161        self
162    }
163
164    pub fn signature_policy(mut self, policy: SignaturePolicy) -> Self {
165        self.signature_policy = policy;
166        self
167    }
168
169    pub fn credentials(mut self, credentials: RegistryCredentials) -> Self {
170        self.credentials = Some(credentials);
171        self
172    }
173
174    fn validate(&self) -> Result<()> {
175        ImageReference::parse(&self.reference).map_err(ClientError::Runtime)?;
176        if let Some(credentials) = &self.credentials {
177            credentials.validate()?;
178        }
179        validate_signature_policy(&self.signature_policy)?;
180        Ok(())
181    }
182
183    fn registry_auth(&self) -> Result<RegistryAuth> {
184        match self.credentials.clone() {
185            Some(credentials) => Ok(credentials.into_auth()),
186            None => {
187                let parsed =
188                    ImageReference::parse(&self.reference).map_err(ClientError::Runtime)?;
189                Ok(RegistryAuth::from_credential_store(&parsed.registry))
190            }
191        }
192    }
193}
194
195/// Request to build an OCI image through the runtime Dockerfile build engine.
196#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct BuildImage {
198    pub context_dir: PathBuf,
199    pub dockerfile_path: PathBuf,
200    pub tag: Option<String>,
201    pub build_args: HashMap<String, String>,
202    pub quiet: bool,
203    pub platforms: Vec<Platform>,
204    pub target: Option<String>,
205    pub no_cache: bool,
206}
207
208impl BuildImage {
209    pub fn new(context_dir: impl Into<PathBuf>) -> Self {
210        let context_dir = context_dir.into();
211        Self {
212            dockerfile_path: context_dir.join("Dockerfile"),
213            context_dir,
214            tag: None,
215            build_args: HashMap::new(),
216            quiet: false,
217            platforms: Vec::new(),
218            target: None,
219            no_cache: false,
220        }
221    }
222
223    pub fn dockerfile_path(mut self, path: impl Into<PathBuf>) -> Self {
224        self.dockerfile_path = path.into();
225        self
226    }
227
228    pub fn tag(mut self, tag: impl Into<String>) -> Self {
229        self.tag = Some(tag.into());
230        self
231    }
232
233    pub fn build_arg(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
234        self.build_args.insert(key.into(), value.into());
235        self
236    }
237
238    pub fn quiet(mut self, quiet: bool) -> Self {
239        self.quiet = quiet;
240        self
241    }
242
243    pub fn platform(mut self, platform: Platform) -> Self {
244        self.platforms.push(platform);
245        self
246    }
247
248    pub fn target(mut self, target: impl Into<String>) -> Self {
249        self.target = Some(target.into());
250        self
251    }
252
253    pub fn no_cache(mut self, no_cache: bool) -> Self {
254        self.no_cache = no_cache;
255        self
256    }
257
258    fn validate(&self) -> Result<()> {
259        if !self.context_dir.exists() {
260            return Err(ClientError::Validation(format!(
261                "build context does not exist: {}",
262                self.context_dir.display()
263            )));
264        }
265        if !self.dockerfile_path.exists() {
266            return Err(ClientError::Validation(format!(
267                "Dockerfile does not exist: {}",
268                self.dockerfile_path.display()
269            )));
270        }
271        Ok(())
272    }
273}
274
275/// Result of an image build.
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277pub struct BuildImageSummary {
278    pub reference: String,
279    pub digest: String,
280    pub size_bytes: u64,
281    pub layer_count: usize,
282}
283
284impl From<RuntimeBuildResult> for BuildImageSummary {
285    fn from(result: RuntimeBuildResult) -> Self {
286        Self {
287            reference: result.reference,
288            digest: result.digest,
289            size_bytes: result.size,
290            layer_count: result.layer_count,
291        }
292    }
293}
294
295/// Request to push a locally cached image through the runtime registry pusher.
296#[derive(Debug, Clone, PartialEq, Eq)]
297pub struct PushImage {
298    pub source: String,
299    pub target: String,
300    pub credentials: Option<RegistryCredentials>,
301    pub registry_protocol: RegistryProtocol,
302}
303
304impl PushImage {
305    pub fn new(source: impl Into<String>, target: impl Into<String>) -> Self {
306        Self {
307            source: source.into(),
308            target: target.into(),
309            credentials: None,
310            registry_protocol: RegistryProtocol::from_env(),
311        }
312    }
313
314    pub fn credentials(mut self, credentials: RegistryCredentials) -> Self {
315        self.credentials = Some(credentials);
316        self
317    }
318
319    pub fn registry_protocol(mut self, protocol: RegistryProtocol) -> Self {
320        self.registry_protocol = protocol;
321        self
322    }
323
324    pub fn plain_http(mut self, enabled: bool) -> Self {
325        if enabled {
326            self.registry_protocol = RegistryProtocol::Http;
327        }
328        self
329    }
330
331    fn validate(&self) -> Result<()> {
332        if self.source.trim().is_empty() {
333            return Err(ClientError::Validation(
334                "source image reference cannot be empty".to_string(),
335            ));
336        }
337        ImageReference::parse(&self.target).map_err(ClientError::Runtime)?;
338        if let Some(credentials) = &self.credentials {
339            credentials.validate()?;
340        }
341        Ok(())
342    }
343
344    fn registry_auth(&self, target: &ImageReference) -> RegistryAuth {
345        match self.credentials.clone() {
346            Some(credentials) => credentials.into_auth(),
347            None => RegistryAuth::from_credential_store(&target.registry),
348        }
349    }
350}
351
352fn validate_signature_policy(policy: &SignaturePolicy) -> Result<()> {
353    match policy {
354        SignaturePolicy::Skip => Ok(()),
355        SignaturePolicy::CosignKey { public_key } if public_key.trim().is_empty() => {
356            Err(ClientError::Validation(
357                "cosign public key path cannot be empty".to_string(),
358            ))
359        }
360        SignaturePolicy::CosignKeyless { issuer, identity }
361            if issuer.trim().is_empty() || identity.trim().is_empty() =>
362        {
363            Err(ClientError::Validation(
364                "cosign keyless issuer and identity cannot be empty".to_string(),
365            ))
366        }
367        _ => Ok(()),
368    }
369}
370
371/// Result of an image push.
372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
373pub struct PushImageSummary {
374    pub reference: String,
375    pub manifest_digest: String,
376    pub config_url: String,
377    pub manifest_url: String,
378}
379
380impl PushImageSummary {
381    fn from_push_result(reference: String, result: PushResult) -> Self {
382        Self {
383            reference,
384            manifest_digest: result.manifest_digest,
385            config_url: result.config_url,
386            manifest_url: result.manifest_url,
387        }
388    }
389}
390
391/// Request to add a tag to a cached image.
392#[derive(Debug, Clone, PartialEq, Eq)]
393pub struct TagImage {
394    pub source: String,
395    pub target: String,
396}
397
398impl TagImage {
399    pub fn new(source: impl Into<String>, target: impl Into<String>) -> Self {
400        Self {
401            source: source.into(),
402            target: target.into(),
403        }
404    }
405
406    fn validate(&self) -> Result<()> {
407        if self.source.trim().is_empty() {
408            return Err(ClientError::Validation(
409                "source image reference cannot be empty".to_string(),
410            ));
411        }
412        validate_tag_target(&self.target)
413    }
414}
415
416/// Options for stopping a running or paused box.
417#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
418pub struct StopBox {
419    pub timeout_secs: Option<u64>,
420}
421
422impl StopBox {
423    pub fn new() -> Self {
424        Self::default()
425    }
426
427    pub fn timeout_secs(mut self, timeout_secs: u64) -> Self {
428        self.timeout_secs = Some(timeout_secs);
429        self
430    }
431}
432
433/// Options for removing a box.
434#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
435pub struct RemoveBox {
436    pub force: bool,
437}
438
439impl RemoveBox {
440    pub fn new() -> Self {
441        Self::default()
442    }
443
444    pub fn force(mut self, force: bool) -> Self {
445        self.force = force;
446        self
447    }
448}
449
450/// Result of removing a box.
451#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
452pub struct RemoveBoxSummary {
453    pub id: String,
454    pub name: String,
455}
456
457/// Request to create a snapshot from a box.
458#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
459pub struct CreateSnapshot {
460    pub name: Option<String>,
461    pub description: Option<String>,
462}
463
464impl CreateSnapshot {
465    pub fn new() -> Self {
466        Self::default()
467    }
468
469    pub fn name(mut self, name: impl Into<String>) -> Self {
470        self.name = Some(name.into());
471        self
472    }
473
474    pub fn description(mut self, description: impl Into<String>) -> Self {
475        self.description = Some(description.into());
476        self
477    }
478
479    fn validate(&self) -> Result<()> {
480        if let Some(name) = &self.name {
481            validate_name("snapshot", name)?;
482        }
483        Ok(())
484    }
485}
486
487/// Request to restore a snapshot into a new box record.
488#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
489pub struct RestoreSnapshot {
490    pub name: Option<String>,
491}
492
493impl RestoreSnapshot {
494    pub fn new() -> Self {
495        Self::default()
496    }
497
498    pub fn name(mut self, name: impl Into<String>) -> Self {
499        self.name = Some(name.into());
500        self
501    }
502
503    fn validate(&self) -> Result<()> {
504        if let Some(name) = &self.name {
505            validate_name("box", name)?;
506        }
507        Ok(())
508    }
509}
510
511/// How a stop request completed.
512#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
513pub enum StopOutcome {
514    AlreadyExited,
515    GracefulExit,
516    ForceKilled,
517}
518
519impl StopOutcome {
520    fn inferred_exit_code(self, stop_signal: i32) -> Option<i32> {
521        match self {
522            Self::AlreadyExited => None,
523            Self::GracefulExit => Some(128 + stop_signal),
524            Self::ForceKilled => Some(137),
525        }
526    }
527}
528
529/// Result of stopping a box.
530#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
531pub struct StopBoxSummary {
532    pub id: String,
533    pub name: String,
534    pub outcome: StopOutcome,
535    pub exit_code: Option<i32>,
536    pub auto_removed: bool,
537    pub box_summary: Option<BoxSummary>,
538}