1#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3pub struct BoxSummary {
4 pub id: String,
5 pub short_id: String,
6 pub name: String,
7 pub image: String,
8 pub isolation: a3s_box_core::ExecutionIsolation,
9 pub status: String,
10 pub status_summary: String,
11 pub active: bool,
12 pub pid: Option<u32>,
13 pub cpus: u32,
14 pub memory_mb: u32,
15 pub ports: Vec<String>,
16 pub command: Vec<String>,
17 pub health: String,
18 pub labels: HashMap<String, String>,
19 pub created_at: String,
20 pub started_at: Option<String>,
21 pub network_name: Option<String>,
22 pub volume_names: Vec<String>,
23}
24
25impl BoxSummary {
26 fn from_record(record: &BoxRecord) -> Self {
27 Self {
28 id: record.id.clone(),
29 short_id: record.short_id.clone(),
30 name: record.name.clone(),
31 image: record.image.clone(),
32 isolation: record.isolation,
33 status: record.status.clone(),
34 status_summary: record.status_summary(),
35 active: record.is_active(),
36 pid: record.pid,
37 cpus: record.cpus,
38 memory_mb: record.memory_mb,
39 ports: record.port_map.clone(),
40 command: record.cmd.clone(),
41 health: record.health_status.clone(),
42 labels: record.labels.clone(),
43 created_at: record.created_at.to_rfc3339(),
44 started_at: record.started_at.map(|ts| ts.to_rfc3339()),
45 network_name: record.network_name.clone(),
46 volume_names: record.volume_names.clone(),
47 }
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct BoxLogLine {
54 pub stream: String,
55 pub timestamp: Option<String>,
56 pub message: String,
57}
58
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61pub struct BoxStatsSummary {
62 pub id: String,
63 pub short_id: String,
64 pub name: String,
65 pub status: String,
66 pub pid: u32,
67 pub cpus: u32,
68 pub cpu_percent: f32,
69 pub cpu_percent_scaled: f64,
70 pub memory_bytes: u64,
71 pub memory_limit_bytes: u64,
72 pub memory_percent: f64,
73 pub network_rx_bytes: u64,
74 pub network_tx_bytes: u64,
75 pub block_read_bytes: u64,
76 pub block_write_bytes: u64,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct RuntimeDiagnostics {
82 pub core_version: String,
83 pub runtime_version: String,
84 pub sdk_version: String,
85 pub home: PathBuf,
86 pub virtualization: RuntimeVirtualizationSummary,
87}
88
89impl RuntimeDiagnostics {
90 fn collect(paths: &A3sBoxPaths) -> Self {
91 Self {
92 core_version: a3s_box_core::VERSION.to_string(),
93 runtime_version: a3s_box_runtime::VERSION.to_string(),
94 sdk_version: env!("CARGO_PKG_VERSION").to_string(),
95 home: paths.home.clone(),
96 virtualization: RuntimeVirtualizationSummary::collect(),
97 }
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct RuntimeDiskUsage {
104 pub home: PathBuf,
105 pub total_bytes: u64,
106 pub boxes_bytes: u64,
107 pub images_bytes: u64,
108 pub volumes_bytes: u64,
109 pub snapshots_bytes: u64,
110 pub state_bytes: u64,
111 pub other_bytes: u64,
112}
113
114impl RuntimeDiskUsage {
115 fn collect(paths: &A3sBoxPaths) -> Result<Self> {
116 let boxes_dir = paths.home.join("boxes");
117 let boxes_bytes = disk_usage_path(&boxes_dir)?;
118 let images_bytes = disk_usage_path(&paths.images_dir)?;
119 let volumes_bytes = disk_usage_path(&paths.volumes_dir)?;
120 let snapshots_bytes = disk_usage_path(&paths.snapshots_dir)?;
121 let state_bytes = disk_usage_paths(&[
122 paths.boxes_file.as_path(),
123 paths.volumes_file.as_path(),
124 paths.networks_file.as_path(),
125 ])?;
126
127 let known_bytes = boxes_bytes
128 .saturating_add(images_bytes)
129 .saturating_add(volumes_bytes)
130 .saturating_add(snapshots_bytes)
131 .saturating_add(state_bytes);
132 let known_home_bytes = [
133 (boxes_dir.as_path(), boxes_bytes),
134 (paths.images_dir.as_path(), images_bytes),
135 (paths.volumes_dir.as_path(), volumes_bytes),
136 (paths.snapshots_dir.as_path(), snapshots_bytes),
137 (
138 paths.boxes_file.as_path(),
139 file_size_or_zero(&paths.boxes_file)?,
140 ),
141 (
142 paths.volumes_file.as_path(),
143 file_size_or_zero(&paths.volumes_file)?,
144 ),
145 (
146 paths.networks_file.as_path(),
147 file_size_or_zero(&paths.networks_file)?,
148 ),
149 ]
150 .into_iter()
151 .filter(|(path, _)| path.starts_with(&paths.home))
152 .map(|(_, bytes)| bytes)
153 .fold(0u64, u64::saturating_add);
154 let home_bytes = disk_usage_path(&paths.home)?;
155 let other_bytes = home_bytes.saturating_sub(known_home_bytes);
156
157 Ok(Self {
158 home: paths.home.clone(),
159 total_bytes: known_bytes.saturating_add(other_bytes),
160 boxes_bytes,
161 images_bytes,
162 volumes_bytes,
163 snapshots_bytes,
164 state_bytes,
165 other_bytes,
166 })
167 }
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172pub struct RuntimeVirtualizationSummary {
173 pub available: bool,
174 pub backend: Option<String>,
175 pub details: String,
176}
177
178impl RuntimeVirtualizationSummary {
179 fn collect() -> Self {
180 match a3s_box_runtime::check_virtualization_support() {
181 Ok(support) => Self {
182 available: true,
183 backend: Some(support.backend),
184 details: support.details,
185 },
186 Err(error) => Self {
187 available: false,
188 backend: None,
189 details: error.to_string(),
190 },
191 }
192 }
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
197pub struct ImageSummary {
198 pub reference: String,
199 pub digest: String,
200 pub size_bytes: u64,
201 pub pulled_at: String,
202 pub last_used: String,
203 pub path: PathBuf,
204}
205
206impl From<StoredImage> for ImageSummary {
207 fn from(image: StoredImage) -> Self {
208 Self {
209 reference: image.reference,
210 digest: image.digest,
211 size_bytes: image.size_bytes,
212 pulled_at: image.pulled_at.to_rfc3339(),
213 last_used: image.last_used.to_rfc3339(),
214 path: image.path,
215 }
216 }
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
221pub struct ImageInspectSummary {
222 pub reference: String,
223 pub digest: String,
224 pub size_bytes: u64,
225 pub pulled_at: String,
226 pub last_used: String,
227 pub path: PathBuf,
228 pub manifest_digest: String,
229 pub layer_count: usize,
230 pub entrypoint: Option<Vec<String>>,
231 pub command: Option<Vec<String>>,
232 pub env: HashMap<String, String>,
233 pub working_dir: Option<String>,
234 pub user: Option<String>,
235 pub exposed_ports: Vec<String>,
236 pub volumes: Vec<String>,
237 pub stop_signal: Option<String>,
238 pub health_check: Option<ImageHealthCheckSummary>,
239 pub onbuild: Vec<String>,
240 pub labels: HashMap<String, String>,
241}
242
243impl ImageInspectSummary {
244 fn from_stored_image(image: StoredImage) -> Result<Self> {
245 let oci = OciImage::from_path(&image.path)?;
246 let config = oci.config();
247 Ok(Self {
248 reference: image.reference,
249 digest: image.digest,
250 size_bytes: image.size_bytes,
251 pulled_at: image.pulled_at.to_rfc3339(),
252 last_used: image.last_used.to_rfc3339(),
253 path: image.path,
254 manifest_digest: oci.manifest_digest().to_string(),
255 layer_count: oci.layer_paths().len(),
256 entrypoint: config.entrypoint.clone(),
257 command: config.cmd.clone(),
258 env: config.env.iter().cloned().collect(),
259 working_dir: config.working_dir.clone(),
260 user: config.user.clone(),
261 exposed_ports: config.exposed_ports.clone(),
262 volumes: config.volumes.clone(),
263 stop_signal: config.stop_signal.clone(),
264 health_check: config
265 .health_check
266 .clone()
267 .map(ImageHealthCheckSummary::from),
268 onbuild: config.onbuild.clone(),
269 labels: config.labels.clone(),
270 })
271 }
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
276pub struct ImageHealthCheckSummary {
277 pub test: Vec<String>,
278 pub interval: Option<u64>,
279 pub timeout: Option<u64>,
280 pub retries: Option<u32>,
281 pub start_period: Option<u64>,
282}
283
284impl From<a3s_box_runtime::oci::OciHealthCheck> for ImageHealthCheckSummary {
285 fn from(health_check: a3s_box_runtime::oci::OciHealthCheck) -> Self {
286 Self {
287 test: health_check.test,
288 interval: health_check.interval,
289 timeout: health_check.timeout,
290 retries: health_check.retries,
291 start_period: health_check.start_period,
292 }
293 }
294}
295
296#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
298pub struct ImageHistoryEntry {
299 pub created: Option<String>,
300 pub created_by: String,
301 pub size_bytes: u64,
302 pub comment: String,
303 pub empty_layer: bool,
304}
305
306#[derive(Debug, Clone, PartialEq, Eq)]
308pub struct CreateVolume {
309 pub name: String,
310 pub driver: String,
311 pub labels: HashMap<String, String>,
312 pub size_limit: u64,
313}
314
315impl CreateVolume {
316 pub fn new(name: impl Into<String>) -> Self {
317 Self {
318 name: name.into(),
319 driver: "local".to_string(),
320 labels: HashMap::new(),
321 size_limit: 0,
322 }
323 }
324
325 pub fn driver(mut self, driver: impl Into<String>) -> Self {
326 self.driver = driver.into();
327 self
328 }
329
330 pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
331 self.labels.insert(key.into(), value.into());
332 self
333 }
334
335 pub fn size_limit(mut self, bytes: u64) -> Self {
336 self.size_limit = bytes;
337 self
338 }
339
340 fn validate(&self) -> Result<()> {
341 validate_name("volume", &self.name)?;
342 if self.driver != "local" {
343 return Err(ClientError::Validation(format!(
344 "unsupported volume driver '{}'; only 'local' is supported",
345 self.driver
346 )));
347 }
348 Ok(())
349 }
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
354pub struct VolumeSummary {
355 pub name: String,
356 pub driver: String,
357 pub mount_point: String,
358 pub labels: HashMap<String, String>,
359 pub in_use_by: Vec<String>,
360 pub in_use: bool,
361 pub size_limit: u64,
362 pub created_at: String,
363}
364
365impl From<VolumeConfig> for VolumeSummary {
366 fn from(volume: VolumeConfig) -> Self {
367 Self {
368 in_use: volume.is_in_use(),
369 name: volume.name,
370 driver: volume.driver,
371 mount_point: volume.mount_point,
372 labels: volume.labels,
373 in_use_by: volume.in_use_by,
374 size_limit: volume.size_limit,
375 created_at: volume.created_at,
376 }
377 }
378}
379
380#[derive(Debug, Clone, PartialEq, Eq)]
382pub struct CreateNetwork {
383 pub name: String,
384 pub subnet: String,
385 pub driver: String,
386 pub labels: HashMap<String, String>,
387 pub isolation: IsolationMode,
388}
389
390impl CreateNetwork {
391 pub fn new(name: impl Into<String>) -> Self {
392 Self {
393 name: name.into(),
394 subnet: "10.89.0.0/24".to_string(),
395 driver: "bridge".to_string(),
396 labels: HashMap::new(),
397 isolation: IsolationMode::None,
398 }
399 }
400
401 pub fn subnet(mut self, subnet: impl Into<String>) -> Self {
402 self.subnet = subnet.into();
403 self
404 }
405
406 pub fn driver(mut self, driver: impl Into<String>) -> Self {
407 self.driver = driver.into();
408 self
409 }
410
411 pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
412 self.labels.insert(key.into(), value.into());
413 self
414 }
415
416 pub fn isolation(mut self, isolation: IsolationMode) -> Self {
417 self.isolation = isolation;
418 self
419 }
420
421 fn validate(&self) -> Result<()> {
422 validate_name("network", &self.name)?;
423 if self.driver != "bridge" {
424 return Err(ClientError::Validation(format!(
425 "unsupported network driver '{}'; only 'bridge' is supported",
426 self.driver
427 )));
428 }
429 Ok(())
430 }
431}
432
433#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
435pub struct NetworkSummary {
436 pub name: String,
437 pub driver: String,
438 pub subnet: String,
439 pub gateway: String,
440 pub labels: HashMap<String, String>,
441 pub endpoints: Vec<NetworkEndpointSummary>,
442 pub endpoint_count: usize,
443 pub isolation: String,
444 pub created_at: String,
445}
446
447impl From<NetworkConfig> for NetworkSummary {
448 fn from(network: NetworkConfig) -> Self {
449 let mut endpoints = network
450 .endpoints
451 .into_values()
452 .map(NetworkEndpointSummary::from)
453 .collect::<Vec<_>>();
454 endpoints.sort_by(|a, b| a.box_name.cmp(&b.box_name));
455 Self {
456 name: network.name,
457 driver: network.driver,
458 subnet: network.subnet,
459 gateway: network.gateway.to_string(),
460 labels: network.labels,
461 endpoint_count: endpoints.len(),
462 endpoints,
463 isolation: format!("{:?}", network.policy.isolation).to_lowercase(),
464 created_at: network.created_at,
465 }
466 }
467}
468
469#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
471pub struct NetworkEndpointSummary {
472 pub box_id: String,
473 pub box_name: String,
474 pub aliases: Vec<String>,
475 pub ip_address: String,
476 pub mac_address: String,
477}
478
479impl From<NetworkEndpoint> for NetworkEndpointSummary {
480 fn from(endpoint: NetworkEndpoint) -> Self {
481 Self {
482 box_id: endpoint.box_id,
483 box_name: endpoint.box_name,
484 aliases: endpoint.aliases,
485 ip_address: endpoint.ip_address.to_string(),
486 mac_address: endpoint.mac_address,
487 }
488 }
489}
490
491#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
493pub struct SnapshotSummary {
494 pub id: String,
495 pub name: String,
496 pub source_box_id: String,
497 pub image: String,
498 pub vcpus: u32,
499 pub memory_mb: u32,
500 pub volumes: Vec<String>,
501 pub command: Vec<String>,
502 pub port_map: Vec<String>,
503 pub labels: HashMap<String, String>,
504 pub network_mode: Option<String>,
505 pub size_bytes: u64,
506 pub created_at: String,
507 pub description: String,
508}
509
510impl From<SnapshotMetadata> for SnapshotSummary {
511 fn from(snapshot: SnapshotMetadata) -> Self {
512 Self {
513 id: snapshot.id,
514 name: snapshot.name,
515 source_box_id: snapshot.source_box_id,
516 image: snapshot.image,
517 vcpus: snapshot.vcpus,
518 memory_mb: snapshot.memory_mb,
519 volumes: snapshot.volumes,
520 command: snapshot.cmd,
521 port_map: snapshot.port_map,
522 labels: snapshot.labels,
523 network_mode: snapshot.network_mode,
524 size_bytes: snapshot.size_bytes,
525 created_at: snapshot.created_at.to_rfc3339(),
526 description: snapshot.description,
527 }
528 }
529}