sui_dockerfile_wrapper/
command.rs1use std::path::Path;
10use std::process::Command;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct DockerBuildInvocation {
16 pub program: String,
18 pub args: Vec<String>,
21}
22
23impl DockerBuildInvocation {
24 #[must_use]
27 pub fn build(
28 dockerfile_path: &Path,
29 context_dir: &Path,
30 image_tag: &str,
31 build_args: &std::collections::BTreeMap<String, String>,
32 ) -> Self {
33 let mut args = vec![
34 "build".to_string(),
35 "-f".to_string(),
36 dockerfile_path.display().to_string(),
37 "-t".to_string(),
38 image_tag.to_string(),
39 ];
40 for (k, v) in build_args {
41 args.push("--build-arg".to_string());
42 let mut kv = k.clone();
43 kv.push('=');
44 kv.push_str(v);
45 args.push(kv);
46 }
47 args.push(context_dir.display().to_string());
48 Self { program: "docker".to_string(), args }
49 }
50
51 #[must_use]
55 pub fn pull(image_ref: &str) -> Self {
56 Self {
57 program: "docker".to_string(),
58 args: vec!["pull".to_string(), image_ref.to_string()],
59 }
60 }
61
62 #[must_use]
67 pub fn history(image_ref: &str) -> Self {
68 Self {
69 program: "docker".to_string(),
70 args: vec![
71 "history".to_string(),
72 "--no-trunc".to_string(),
73 "--format".to_string(),
74 "{{.CreatedBy}}".to_string(),
75 image_ref.to_string(),
76 ],
77 }
78 }
79
80 #[must_use]
84 pub fn save(image_ref: &str, output_path: &Path) -> Self {
85 Self {
86 program: "docker".to_string(),
87 args: vec![
88 "save".to_string(),
89 "-o".to_string(),
90 output_path.display().to_string(),
91 image_ref.to_string(),
92 ],
93 }
94 }
95
96 #[must_use]
101 pub fn push(image_ref: &str) -> Self {
102 Self { program: "docker".to_string(), args: vec!["push".to_string(), image_ref.to_string()] }
103 }
104
105 #[must_use]
107 pub fn tag(source_ref: &str, target_ref: &str) -> Self {
108 Self {
109 program: "docker".to_string(),
110 args: vec!["tag".to_string(), source_ref.to_string(), target_ref.to_string()],
111 }
112 }
113
114 #[must_use]
116 pub fn to_std_command(&self) -> Command {
117 let mut cmd = Command::new(&self.program);
118 cmd.args(&self.args);
119 cmd
120 }
121}
122
123#[derive(Debug, Clone)]
126pub struct CommandOutcome {
127 pub success: bool,
128 pub exit_code: Option<i32>,
129 pub stdout: Vec<u8>,
130 pub stderr: Vec<u8>,
131}
132
133impl CommandOutcome {
134 #[must_use]
138 pub fn stderr_tail(&self, n: usize) -> String {
139 let start = self.stderr.len().saturating_sub(n);
140 String::from_utf8_lossy(&self.stderr[start..]).into_owned()
141 }
142}
143
144#[derive(Debug, thiserror::Error)]
147pub enum CommandRunError {
148 #[error("failed to spawn `{program}`: {source}")]
149 Spawn {
150 program: String,
151 #[source]
152 source: std::io::Error,
153 },
154}
155
156pub trait CommandRunner {
159 fn run(&self, invocation: &DockerBuildInvocation) -> Result<CommandOutcome, CommandRunError>;
168}
169
170#[derive(Debug, Default, Clone, Copy)]
173pub struct RealCommandRunner;
174
175impl CommandRunner for RealCommandRunner {
176 fn run(&self, invocation: &DockerBuildInvocation) -> Result<CommandOutcome, CommandRunError> {
177 let output = invocation
178 .to_std_command()
179 .output()
180 .map_err(|source| CommandRunError::Spawn { program: invocation.program.clone(), source })?;
181 Ok(CommandOutcome {
182 success: output.status.success(),
183 exit_code: output.status.code(),
184 stdout: output.stdout,
185 stderr: output.stderr,
186 })
187 }
188}
189
190#[derive(Debug, Default)]
194pub struct MockCommandRunner {
195 pub invocations: std::sync::Mutex<Vec<DockerBuildInvocation>>,
196 pub scripted_outcome: Option<CommandOutcome>,
197}
198
199impl MockCommandRunner {
200 #[must_use]
201 pub fn new() -> Self {
202 Self::default()
203 }
204
205 #[must_use]
206 pub fn with_outcome(outcome: CommandOutcome) -> Self {
207 Self { invocations: std::sync::Mutex::new(Vec::new()), scripted_outcome: Some(outcome) }
208 }
209
210 #[must_use]
217 pub fn recorded(&self) -> Vec<DockerBuildInvocation> {
218 self.invocations.lock().expect("mock mutex poisoned").clone()
219 }
220}
221
222impl CommandRunner for MockCommandRunner {
223 fn run(&self, invocation: &DockerBuildInvocation) -> Result<CommandOutcome, CommandRunError> {
224 self.invocations
225 .lock()
226 .expect("mock mutex poisoned")
227 .push(invocation.clone());
228 Ok(self.scripted_outcome.clone().unwrap_or(CommandOutcome {
229 success: true,
230 exit_code: Some(0),
231 stdout: Vec::new(),
232 stderr: Vec::new(),
233 }))
234 }
235}
236