1pub type Result<T> = std::result::Result<T, ClientError>;
3
4#[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#[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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct ListBoxesOptions {
60 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub struct ReadBoxLogsOptions {
83 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
452pub struct RemoveBoxSummary {
453 pub id: String,
454 pub name: String,
455}
456
457#[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#[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#[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#[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}