1use std::process::{Command, Stdio};
2
3use anyhow::{Context, Result, bail};
4use serde_json::Value;
5
6#[derive(Debug)]
7struct CommandOutput {
8 success: bool,
9 stdout: Vec<u8>,
10 stderr: Vec<u8>,
11}
12
13pub fn system_df() -> Result<String> {
19 run_docker(&["system", "df", "-v"])
20}
21
22pub fn system_df_structured() -> Result<Vec<Value>> {
28 let output = run_docker(&["system", "df", "--format", "{{json .}}"])?;
29 parse_system_df(&output)
30}
31
32fn parse_system_df(output: &str) -> Result<Vec<Value>> {
33 output
34 .lines()
35 .filter(|line| !line.trim().is_empty())
36 .map(|line| serde_json::from_str(line).context("Docker emitted an invalid JSON row"))
37 .collect()
38}
39
40pub fn prune_build_cache(older_than: Option<&str>) -> Result<String> {
46 let mut arguments = vec!["builder", "prune", "-af"];
47 let filter;
48 if let Some(age) = older_than {
49 filter = format!("until={age}");
50 arguments.extend(["--filter", &filter]);
51 }
52 run_docker(&arguments)
53}
54
55pub fn prune_system(older_than: Option<&str>) -> Result<String> {
63 let mut arguments = vec!["system", "prune", "-af"];
64 let filter;
65 if let Some(age) = older_than {
66 filter = format!("until={age}");
67 arguments.extend(["--filter", &filter]);
68 }
69 run_docker(&arguments)
70}
71
72fn run_docker(arguments: &[&str]) -> Result<String> {
73 run_docker_with(arguments, |arguments| {
74 let output = Command::new("docker")
75 .args(arguments)
76 .stdin(Stdio::null())
77 .output()
78 .context("failed to run docker")?;
79 Ok(CommandOutput {
80 success: output.status.success(),
81 stdout: output.stdout,
82 stderr: output.stderr,
83 })
84 })
85}
86
87fn run_docker_with(
88 arguments: &[&str],
89 runner: impl FnOnce(&[&str]) -> Result<CommandOutput>,
90) -> Result<String> {
91 let output = runner(arguments)?;
92 if !output.success {
93 let stderr = String::from_utf8_lossy(&output.stderr);
94 bail!("docker failed: {}", stderr.trim());
95 }
96 Ok(String::from_utf8_lossy(&output.stdout).into_owned())
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn docker_runner_should_forward_arguments_and_surface_stderr() {
105 let result = run_docker_with(&["builder", "prune", "-af"], |arguments| {
106 assert_eq!(arguments, ["builder", "prune", "-af"]);
107 Ok(CommandOutput {
108 success: false,
109 stdout: Vec::new(),
110 stderr: b"daemon unavailable".to_vec(),
111 })
112 });
113
114 assert!(
115 result
116 .expect_err("failure status must be rejected")
117 .to_string()
118 .contains("daemon unavailable")
119 );
120 }
121
122 #[test]
123 fn structured_rows_should_parse_json_lines() -> Result<()> {
124 let output =
125 "{\"Type\":\"Images\",\"Size\":\"1GB\"}\n{\"Type\":\"Build Cache\",\"Size\":\"2GB\"}\n";
126 let rows = parse_system_df(output)?;
127
128 assert_eq!(rows.len(), 2);
129 assert_eq!(rows[1]["Type"], "Build Cache");
130 Ok(())
131 }
132}