blue_build_process_management/drivers/
buildah_driver.rs1use blue_build_utils::{
2 container::ContainerId, credentials::Credentials, secret::SecretArgs, semver::Version, sudo_cmd,
3};
4use colored::Colorize;
5use comlexr::{cmd, pipe};
6use log::{debug, error, info, trace, warn};
7use miette::{Context, IntoDiagnostic, Result, bail};
8use oci_client::Reference;
9use serde::Deserialize;
10use tempfile::TempDir;
11
12use crate::logging::CommandLogging;
13
14use super::{
15 BuildDriver, DriverVersion, ImageStorageDriver,
16 opts::{
17 BuildOpts, ManifestCreateOpts, ManifestPushOpts, PruneOpts, PullOpts, PushOpts, TagOpts,
18 UntagOpts,
19 },
20};
21
22const SUDO_PROMPT: &str = "Password for %u required to run 'buildah' as privileged";
23
24#[derive(Debug, Deserialize)]
25struct BuildahVersionJson {
26 pub version: Version,
27}
28
29#[derive(Debug)]
30pub struct BuildahDriver;
31
32impl DriverVersion for BuildahDriver {
33 const VERSION_REQ: &'static str = ">=1.29";
35
36 fn version() -> Result<Version> {
37 trace!("BuildahDriver::version()");
38
39 let output = {
40 let c = cmd!("buildah", "version", "--json");
41 trace!("{c:?}");
42 c
43 }
44 .output()
45 .into_diagnostic()?;
46
47 let version_json: BuildahVersionJson = serde_json::from_slice(&output.stdout)
48 .inspect_err(|e| error!("{e}: {}", String::from_utf8_lossy(&output.stdout)))
49 .into_diagnostic()?;
50 trace!("{version_json:#?}");
51
52 Ok(version_json.version)
53 }
54}
55
56impl BuildDriver for BuildahDriver {
57 fn build(opts: BuildOpts) -> Result<()> {
58 trace!("BuildahDriver::build({opts:#?})");
59
60 let temp_dir = TempDir::new()
61 .into_diagnostic()
62 .wrap_err("Failed to create temporary directory for secrets")?;
63
64 let command = sudo_cmd!(
65 prompt = SUDO_PROMPT,
66 sudo_check = opts.privileged,
67 "buildah",
68 "build",
69 for opts.secrets.args(&temp_dir)?,
70 if opts.secrets.ssh() => "--ssh",
71 if let Some(platform) = opts.platform => [
72 "--platform",
73 platform.to_string(),
74 ],
75 "--pull=true",
76 format!("--layers={}", !opts.squash),
77 match opts.cache_from.as_ref() {
78 Some(cache_from) if !opts.squash => [
79 "--cache-from",
80 format!(
81 "{}/{}",
82 cache_from.registry(),
83 cache_from.repository()
84 ),
85 ],
86 _ => [],
87 },
88 match opts.cache_from.as_ref() {
89 Some(cache_to) if !opts.squash => [
90 "--cache-to",
91 format!(
92 "{}/{}",
93 cache_to.registry(),
94 cache_to.repository()
95 ),
96 ],
97 _ => [],
98 },
99 "-f",
100 opts.containerfile,
101 "-t",
102 opts.image.to_string(),
103 );
104
105 trace!("{command:?}");
106 let status = command
107 .build_status(opts.image.to_string(), "Building Image")
108 .into_diagnostic()?;
109
110 if status.success() {
111 info!("Successfully built {}", opts.image);
112 } else {
113 bail!("Failed to build {}", opts.image);
114 }
115 Ok(())
116 }
117
118 fn tag(opts: TagOpts) -> Result<()> {
119 trace!("BuildahDriver::tag({opts:#?})");
120
121 let dest_image_str = opts.dest_image.to_string();
122
123 let mut command = sudo_cmd!(
124 prompt = SUDO_PROMPT,
125 sudo_check = opts.privileged,
126 "buildah",
127 "tag",
128 opts.src_image.to_string(),
129 &dest_image_str,
130 );
131
132 trace!("{command:?}");
133 if command.status().into_diagnostic()?.success() {
134 info!("Successfully tagged {}!", dest_image_str.bold().green());
135 } else {
136 bail!("Failed to tag image {}", dest_image_str.bold().red());
137 }
138 Ok(())
139 }
140
141 fn untag(opts: UntagOpts) -> Result<()> {
142 trace!("BuildahDriver::untag({opts:#?})");
143
144 let ref_string = opts.image.to_string();
145
146 let mut command = sudo_cmd!(
147 prompt = SUDO_PROMPT,
148 sudo_check = opts.privileged,
149 "buildah",
150 "untag",
151 &ref_string, &ref_string, );
154
155 trace!("{command:?}");
156 if command.status().into_diagnostic()?.success() {
157 info!("Successfully untagged {}", ref_string.bold().green());
158 } else {
159 bail!("Failed to untag image {}", ref_string.bold().red());
160 }
161 Ok(())
162 }
163
164 fn push(opts: PushOpts) -> Result<()> {
165 trace!("BuildahDriver::push({opts:#?})");
166
167 let image_str = opts.image.to_string();
168
169 let command = sudo_cmd!(
170 prompt = SUDO_PROMPT,
171 sudo_check = opts.privileged,
172 "buildah",
173 "push",
174 format!(
175 "--compression-format={}",
176 opts.compression_type.unwrap_or_default()
177 ),
178 &image_str,
179 );
180
181 trace!("{command:?}");
182 let status = command
183 .build_status(&image_str, "Pushing Image")
184 .into_diagnostic()?;
185
186 if status.success() {
187 info!("Successfully pushed {}!", image_str.bold().green());
188 } else {
189 bail!("Failed to push image {}", image_str.bold().red());
190 }
191 Ok(())
192 }
193
194 fn pull(opts: PullOpts) -> Result<ContainerId> {
195 trace!("BuildahDriver::pull({opts:#?})");
196
197 let image_str = opts.image.to_string();
198
199 let mut command = sudo_cmd!(
200 prompt = SUDO_PROMPT,
201 sudo_check = opts.privileged,
202 "buildah",
203 "pull",
204 "--quiet",
205 if let Some(retries) = opts.retry_count => format!("--retry={retries}"),
206 if let Some(platform) = opts.platform => format!("--platform={platform}"),
207 &image_str,
208 );
209
210 info!("Pulling image {image_str}...");
211
212 trace!("{command:?}");
213 let output = command.output().into_diagnostic()?;
214
215 if !output.status.success() {
216 bail!("Failed to pull image {}", image_str.bold().red());
217 }
218 info!("Successfully pulled image {}", image_str.bold().green());
219 let container_id = {
220 let mut stdout = output.stdout;
221 while stdout.pop_if(|byte| byte.is_ascii_whitespace()).is_some() {}
222 ContainerId(String::from_utf8(stdout).into_diagnostic()?)
223 };
224 Ok(container_id)
225 }
226
227 fn login(server: &str) -> Result<()> {
228 trace!("BuildahDriver::login()");
229
230 if let Some(Credentials::Basic { username, password }) = Credentials::get(server) {
231 let output = pipe!(
232 stdin = password.value();
233 {
234 let c = cmd!(
235 "buildah",
236 "login",
237 "-u",
238 &username,
239 "--password-stdin",
240 server,
241 );
242 trace!("{c:?}");
243 c
244 }
245 )
246 .output()
247 .into_diagnostic()?;
248
249 if !output.status.success() {
250 let err_out = String::from_utf8_lossy(&output.stderr);
251 bail!("Failed to login for buildah:\n{}", err_out.trim());
252 }
253 debug!("Logged into {server}");
254 }
255 Ok(())
256 }
257
258 fn prune(opts: PruneOpts) -> Result<()> {
259 trace!("BuildahDriver::prune({opts:?})");
260
261 let status = cmd!(
262 "buildah",
263 "prune",
264 "--force",
265 if opts.all => "--all",
266 )
267 .message_status("buildah prune", "Pruning Buildah System")
268 .into_diagnostic()?;
269
270 if !status.success() {
271 bail!("Failed to prune buildah");
272 }
273
274 Ok(())
275 }
276
277 fn manifest_create(opts: ManifestCreateOpts) -> Result<()> {
278 let output = {
279 let c = cmd!("buildah", "manifest", "rm", opts.final_image.to_string());
280 trace!("{c:?}");
281 c
282 }
283 .output()
284 .into_diagnostic()?;
285
286 if output.status.success() {
287 warn!(
288 "Existing image manifest {} exists, removing...",
289 opts.final_image
290 );
291 }
292
293 let output = {
294 let c = cmd!(
295 "buildah",
296 "manifest",
297 "create",
298 opts.final_image.to_string(),
299 for image in opts.image_list => format!("containers-storage:{image}"),
300 );
301 trace!("{c:?}");
302 c
303 }
304 .output()
305 .into_diagnostic()?;
306
307 if !output.status.success() {
308 bail!(
309 "Failed to create manifest for {}:\n{}",
310 opts.final_image,
311 String::from_utf8_lossy(&output.stderr)
312 );
313 }
314
315 Ok(())
316 }
317
318 fn manifest_push(opts: ManifestPushOpts) -> Result<()> {
319 let image = &opts.final_image.to_string();
320 let status = {
321 let c = cmd!(
322 "buildah",
323 "manifest",
324 "push",
325 "--all",
326 if let Some(compression_fmt) = opts.compression_type => format!(
327 "--compression-format={compression_fmt}"
328 ),
329 image,
330 format!("docker://{}", opts.final_image),
331 );
332 trace!("{c:?}");
333 c
334 }
335 .build_status(image, format!("Pushing manifest {image}..."))
336 .into_diagnostic()?;
337
338 if !status.success() {
339 bail!("Failed to create manifest for {}", opts.final_image);
340 }
341
342 Ok(())
343 }
344}
345
346impl ImageStorageDriver for BuildahDriver {
347 fn remove_image(opts: super::opts::RemoveImageOpts) -> Result<()> {
348 trace!("BuildahDriver::remove_image({opts:?})");
349
350 let output = {
351 let c = sudo_cmd!(
352 prompt = SUDO_PROMPT,
353 sudo_check = opts.privileged,
354 "buildah",
355 "rmi",
356 opts.image.to_string(),
357 );
358 trace!("{c:?}");
359 c
360 }
361 .output()
362 .into_diagnostic()?;
363
364 if !output.status.success() {
365 let err_out = String::from_utf8_lossy(&output.stderr);
366 bail!(
367 "Failed to remove the image {}:\n{}",
368 opts.image,
369 err_out.trim()
370 );
371 }
372
373 Ok(())
374 }
375
376 fn list_images(privileged: bool) -> Result<Vec<Reference>> {
377 #[derive(Deserialize)]
378 #[serde(rename_all = "PascalCase")]
379 struct Image {
380 names: Option<Vec<String>>,
381 }
382
383 trace!("BuildahDriver::list_images({privileged})");
384
385 let output = {
386 let c = sudo_cmd!(
387 prompt = SUDO_PROMPT,
388 sudo_check = privileged,
389 "buildah",
390 "images",
391 "--json",
392 );
393 trace!("{c:?}");
394 c
395 }
396 .output()
397 .into_diagnostic()?;
398
399 if !output.status.success() {
400 let err_out = String::from_utf8_lossy(&output.stderr);
401 bail!("Failed to list images:\n{}", err_out.trim());
402 }
403
404 let images: Vec<Image> = serde_json::from_slice(&output.stdout).into_diagnostic()?;
405
406 images
407 .into_iter()
408 .filter_map(|image| image.names)
409 .flat_map(|names| {
410 names
411 .into_iter()
412 .map(|name| name.parse::<Reference>().into_diagnostic())
413 })
414 .collect()
415 }
416}