Skip to main content

a3s_box_sdk/client/
summaries.rs

1/// Typed box summary for management UIs.
2#[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/// One decoded box log line.
52#[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/// Host-side resource usage snapshot for one active box.
60#[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/// Local runtime diagnostics suitable for status bars and diagnostics panes.
80#[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/// Local disk usage grouped by runtime-owned state areas.
102#[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/// Host virtualization status reported by the runtime support checker.
171#[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/// Typed image summary for management UIs.
196#[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/// Detailed local OCI image metadata for management UIs.
220#[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/// Docker-compatible image health check metadata.
275#[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/// One OCI image history entry.
297#[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/// Request to create a named volume.
307#[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/// Typed volume summary for management UIs.
353#[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/// Request to create a network.
381#[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/// Typed network summary for management UIs.
434#[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/// Typed network endpoint summary.
470#[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/// Typed snapshot summary for management UIs.
492#[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}