Skip to main content

blue_build_process_management/drivers/
buildah_driver.rs

1use 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    // The prune command wasn't present until 1.29
34    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, // identify image by reference
152            &ref_string, // remove this reference
153        );
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}