Skip to main content

blue_build_process_management/drivers/
podman_driver.rs

1use std::{
2    ops::Not,
3    path::Path,
4    process::{Command, ExitStatus},
5};
6
7use blue_build_utils::{
8    constants::USER,
9    container::{ContainerId, MountId},
10    credentials::Credentials,
11    get_env_var,
12    secret::SecretArgs,
13    semver::Version,
14    sudo_cmd,
15};
16use colored::Colorize;
17use comlexr::{cmd, pipe};
18use log::{debug, error, info, trace, warn};
19use miette::{Context, IntoDiagnostic, Result, bail};
20use oci_client::Reference;
21use serde::Deserialize;
22use tempfile::TempDir;
23
24use super::{
25    BuildChunkedOciDriver, BuildDriver, ContainerMountDriver, DriverVersion, ImageStorageDriver,
26    RechunkDriver, RunDriver,
27    opts::{
28        BuildOpts, ContainerOpts, CreateContainerOpts, ManifestCreateOpts, ManifestPushOpts,
29        PruneOpts, PullOpts, PushOpts, RemoveContainerOpts, RemoveImageOpts, RunOpts, RunOptsEnv,
30        RunOptsVolume, TagOpts, UntagOpts, VolumeOpts,
31    },
32    rpm_ostree_runner::RpmOstreeRunner,
33};
34use crate::{
35    logging::CommandLogging,
36    signal_handler::{ContainerRuntime, ContainerSignalId, DetachedContainer, add_cid, remove_cid},
37};
38
39const SUDO_PROMPT: &str = "Password for %u required to run 'podman' as privileged";
40
41#[derive(Debug, Deserialize)]
42struct PodmanVersionJsonClient {
43    #[serde(alias = "Version")]
44    pub version: Version,
45}
46
47#[derive(Debug, Deserialize)]
48struct PodmanVersionJson {
49    #[serde(alias = "Client")]
50    pub client: PodmanVersionJsonClient,
51}
52
53#[derive(Debug)]
54pub struct PodmanDriver;
55
56impl PodmanDriver {
57    /// Copy an image from the user container
58    /// store to the root container store for
59    /// booting off of.
60    ///
61    /// # Errors
62    /// Will error if the image can't be copied.
63    pub fn copy_image_to_root_store(image: &Reference) -> Result<()> {
64        let image = image.whole();
65        let status = {
66            let c = sudo_cmd!(
67                prompt = SUDO_PROMPT,
68                "podman",
69                "image",
70                "scp",
71                format!("{}@localhost::{image}", get_env_var(USER)?),
72                "root@localhost::"
73            );
74            trace!("{c:?}");
75            c
76        }
77        .build_status(&image, "Copying image to root container store")
78        // .status()
79        .into_diagnostic()?;
80
81        if status.success().not() {
82            bail!(
83                "Failed to copy image {} to root container store",
84                image.bold()
85            );
86        }
87
88        Ok(())
89    }
90}
91
92impl DriverVersion for PodmanDriver {
93    // First podman version to use buildah v1.24
94    // https://github.com/containers/podman/blob/main/RELEASE_NOTES.md#400
95    const VERSION_REQ: &'static str = ">=4";
96
97    fn version() -> Result<Version> {
98        trace!("PodmanDriver::version()");
99
100        let output = {
101            let c = cmd!("podman", "version", "-f", "json");
102            trace!("{c:?}");
103            c
104        }
105        .output()
106        .into_diagnostic()?;
107
108        let version_json: PodmanVersionJson = serde_json::from_slice(&output.stdout)
109            .inspect_err(|e| error!("{e}: {}", String::from_utf8_lossy(&output.stdout)))
110            .into_diagnostic()?;
111        trace!("{version_json:#?}");
112
113        Ok(version_json.client.version)
114    }
115}
116
117impl BuildDriver for PodmanDriver {
118    fn build(opts: BuildOpts) -> Result<()> {
119        trace!("PodmanDriver::build({opts:#?})");
120
121        let temp_dir = TempDir::new()
122            .into_diagnostic()
123            .wrap_err("Failed to create temporary directory for secrets")?;
124
125        let command = sudo_cmd!(
126            prompt = SUDO_PROMPT,
127            sudo_check = opts.privileged,
128            "podman",
129            "build",
130            if let Some(platform) = opts.platform => [
131                "--platform",
132                platform.to_string(),
133            ],
134            match opts.cache_from.as_ref() {
135                Some(cache_from) if !opts.squash => [
136                    "--cache-from",
137                    format!(
138                        "{}/{}",
139                        cache_from.registry(),
140                        cache_from.repository()
141                    ),
142                ],
143                _ => [],
144            },
145            match opts.cache_from.as_ref() {
146                Some(cache_to) if !opts.squash => [
147                    "--cache-to",
148                    format!(
149                        "{}/{}",
150                        cache_to.registry(),
151                        cache_to.repository()
152                    ),
153                ],
154                _ => [],
155            },
156            "--pull=true",
157            if opts.host_network => "--net=host",
158            format!("--layers={}", !opts.squash),
159            "-f",
160            opts.containerfile,
161            "-t",
162            opts.image.to_string(),
163            for opts.secrets.args(&temp_dir)?,
164            if opts.secrets.ssh() => "--ssh",
165            ".",
166        );
167
168        trace!("{command:?}");
169        let status = command
170            .build_status(opts.image.to_string(), "Building Image")
171            .into_diagnostic()?;
172
173        if status.success() {
174            info!("Successfully built {}", opts.image);
175        } else {
176            bail!("Failed to build {}", opts.image);
177        }
178        Ok(())
179    }
180
181    fn tag(opts: TagOpts) -> Result<()> {
182        trace!("PodmanDriver::tag({opts:#?})");
183
184        let dest_image_str = opts.dest_image.to_string();
185
186        let mut command = sudo_cmd!(
187            prompt = SUDO_PROMPT,
188            sudo_check = opts.privileged,
189            "podman",
190            "tag",
191            opts.src_image.to_string(),
192            &dest_image_str
193        );
194
195        trace!("{command:?}");
196        let status = command.status().into_diagnostic()?;
197
198        if status.success() {
199            info!("Successfully tagged {}!", dest_image_str.bold().green());
200        } else {
201            bail!("Failed to tag image {}", dest_image_str.bold().red());
202        }
203        Ok(())
204    }
205
206    fn untag(opts: UntagOpts) -> Result<()> {
207        trace!("PodmanDriver::untag({opts:#?})");
208
209        let ref_string = opts.image.to_string();
210
211        let mut command = sudo_cmd!(
212            prompt = SUDO_PROMPT,
213            sudo_check = opts.privileged,
214            "podman",
215            "untag",
216            &ref_string, // identify image by reference
217            &ref_string, // remove this reference
218        );
219
220        trace!("{command:?}");
221        let status = command.status().into_diagnostic()?;
222
223        if status.success() {
224            info!("Successfully untagged {}", ref_string.bold().green());
225        } else {
226            bail!("Failed to untag image {}", ref_string.bold().red());
227        }
228        Ok(())
229    }
230
231    fn push(opts: PushOpts) -> Result<()> {
232        trace!("PodmanDriver::push({opts:#?})");
233
234        let image_str = opts.image.to_string();
235
236        let command = sudo_cmd!(
237            prompt = SUDO_PROMPT,
238            sudo_check = opts.privileged,
239            "podman",
240            "push",
241            format!(
242                "--compression-format={}",
243                opts.compression_type.unwrap_or_default()
244            ),
245            &image_str,
246        );
247
248        trace!("{command:?}");
249        let status = command
250            .build_status(&image_str, "Pushing Image")
251            .into_diagnostic()?;
252
253        if status.success() {
254            info!("Successfully pushed {}!", image_str.bold().green());
255        } else {
256            bail!("Failed to push image {}", image_str.bold().red());
257        }
258        Ok(())
259    }
260
261    fn pull(opts: PullOpts) -> Result<ContainerId> {
262        trace!("PodmanDriver::pull({opts:#?})");
263
264        let image_str = opts.image.to_string();
265
266        let mut command = sudo_cmd!(
267            prompt = SUDO_PROMPT,
268            sudo_check = opts.privileged,
269            "podman",
270            "pull",
271            "--quiet",
272            if let Some(retries) = opts.retry_count => format!("--retry={retries}"),
273            if let Some(platform) = opts.platform => format!("--platform={platform}"),
274            &image_str,
275        );
276
277        info!("Pulling image {image_str}...");
278
279        trace!("{command:?}");
280        let output = command.output().into_diagnostic()?;
281
282        if !output.status.success() {
283            bail!("Failed to pull image {}", image_str.bold().red());
284        }
285        info!("Successfully pulled image {}", image_str.bold().green());
286        let container_id = {
287            let mut stdout = output.stdout;
288            while stdout.pop_if(|byte| byte.is_ascii_whitespace()).is_some() {}
289            ContainerId(String::from_utf8(stdout).into_diagnostic()?)
290        };
291        Ok(container_id)
292    }
293
294    fn login(server: &str) -> Result<()> {
295        trace!("PodmanDriver::login()");
296
297        if let Some(Credentials::Basic { username, password }) = Credentials::get(server) {
298            let output = pipe!(
299                stdin = password.value();
300                {
301                    let c = cmd!(
302                        "podman",
303                        "login",
304                        "-u",
305                        &username,
306                        "--password-stdin",
307                        server,
308                    );
309                    trace!("{c:?}");
310                    c
311                }
312            )
313            .output()
314            .into_diagnostic()?;
315
316            if !output.status.success() {
317                let err_out = String::from_utf8_lossy(&output.stderr);
318                bail!("Failed to login for podman:\n{}", err_out.trim());
319            }
320            debug!("Logged into {server}");
321        }
322        Ok(())
323    }
324
325    fn prune(opts: PruneOpts) -> Result<()> {
326        trace!("PodmanDriver::prune({opts:?})");
327
328        let status = {
329            let c = cmd!(
330                "podman",
331                "system",
332                "prune",
333                "--force",
334                if opts.all => "--all",
335                if opts.volumes => "--volumes",
336            );
337            trace!("{c:?}");
338            c
339        }
340        .message_status("podman system prune", "Pruning Podman System")
341        .into_diagnostic()?;
342
343        if !status.success() {
344            bail!("Failed to prune podman");
345        }
346
347        Ok(())
348    }
349
350    fn manifest_create(opts: ManifestCreateOpts) -> Result<()> {
351        let output = {
352            let c = cmd!("podman", "manifest", "rm", opts.final_image.to_string());
353            trace!("{c:?}");
354            c
355        }
356        .output()
357        .into_diagnostic()?;
358
359        if output.status.success() {
360            warn!(
361                "Existing image manifest {} exists, removing...",
362                opts.final_image
363            );
364        }
365
366        let output = {
367            let c = cmd!(
368                "podman",
369                "manifest",
370                "create",
371                "--all",
372                opts.final_image.to_string(),
373                for image in opts.image_list => format!("containers-storage:{image}"),
374            );
375            trace!("{c:?}");
376            c
377        }
378        .output()
379        .into_diagnostic()?;
380
381        if !output.status.success() {
382            bail!(
383                "Failed to create manifest for {}:\n{}",
384                opts.final_image,
385                String::from_utf8_lossy(&output.stderr)
386            );
387        }
388
389        Ok(())
390    }
391
392    fn manifest_push(opts: ManifestPushOpts) -> Result<()> {
393        let image = &opts.final_image.to_string();
394        let status = {
395            let c = cmd!(
396                "podman",
397                "manifest",
398                "push",
399                if let Some(compression_fmt) = opts.compression_type => format!(
400                    "--compression-format={compression_fmt}"
401                ),
402                image,
403                format!("docker://{}", opts.final_image),
404            );
405            trace!("{c:?}");
406            c
407        }
408        .build_status(image, format!("Pushing manifest {image}..."))
409        .into_diagnostic()?;
410
411        if !status.success() {
412            bail!("Failed to create manifest for {}", opts.final_image);
413        }
414
415        Ok(())
416    }
417}
418
419impl BuildChunkedOciDriver for PodmanDriver {
420    fn manifest_create_with_runner(
421        runner: &RpmOstreeRunner,
422        opts: ManifestCreateOpts,
423    ) -> Result<()> {
424        trace!("PodmanDriver::manifest_create_with_runner({runner:#?}, {opts:#?})");
425        let (cmd, args) = runner.command_args("podman", &["manifest"]);
426        let output = {
427            let c = cmd!(&cmd, for &args, "rm", opts.final_image.to_string());
428            trace!("{c:?}");
429            c
430        }
431        .output()
432        .into_diagnostic()?;
433
434        if output.status.success() {
435            warn!(
436                "Existing image manifest {} exists, removing...",
437                opts.final_image
438            );
439        }
440
441        let output = {
442            let c = cmd!(
443                &cmd,
444                for &args,
445                "create",
446                "--all",
447                opts.final_image.to_string(),
448                for image in opts.image_list => format!("containers-storage:{image}"),
449            );
450            trace!("{c:?}");
451            c
452        }
453        .output()
454        .into_diagnostic()?;
455
456        if !output.status.success() {
457            bail!(
458                "Failed to create manifest for {}:\n{}",
459                opts.final_image,
460                String::from_utf8_lossy(&output.stderr)
461            );
462        }
463
464        Ok(())
465    }
466
467    fn manifest_push_with_runner(runner: &RpmOstreeRunner, opts: ManifestPushOpts) -> Result<()> {
468        trace!("PodmanDriver::manifest_push_with_runner({runner:#?}, {opts:#?})");
469        let (cmd, args) = runner.command_args("podman", &["manifest"]);
470        let image = &opts.final_image.to_string();
471        let status = {
472            let c = cmd!(
473                cmd,
474                for args,
475                "push",
476                if let Some(authfile) = runner.authfile() => ["--authfile", authfile],
477                if let Some(compression_fmt) = opts.compression_type => format!(
478                    "--compression-format={compression_fmt}"
479                ),
480                image,
481                format!("docker://{}", opts.final_image),
482            );
483            trace!("{c:?}");
484            c
485        }
486        .build_status(image, format!("Pushing manifest {image}..."))
487        .into_diagnostic()?;
488
489        if !status.success() {
490            bail!("Failed to create manifest for {}", opts.final_image);
491        }
492
493        Ok(())
494    }
495
496    fn pull_with_runner(runner: &RpmOstreeRunner, opts: PullOpts) -> Result<ContainerId> {
497        trace!("PodmanDriver::pull_with_runner({runner:#?}, {opts:#?})");
498        let (cmd, args) = runner.command_args("podman", &["pull"]);
499        let image_str = opts.image.to_string();
500        let mut command = cmd!(
501            cmd,
502            for args,
503            "--quiet",
504            if let Some(retries) = opts.retry_count => format!("--retry={retries}"),
505            if let Some(platform) = opts.platform => format!("--platform={platform}"),
506            &image_str,
507        );
508        info!("Pulling image {image_str}...");
509
510        trace!("{command:?}");
511        let output = command.output().into_diagnostic()?;
512
513        if !output.status.success() {
514            bail!("Failed to pull image {}", image_str.bold().red());
515        }
516        info!("Successfully pulled image {}", image_str.bold().green());
517        let container_id = {
518            let mut stdout = output.stdout;
519            while stdout.pop_if(|byte| byte.is_ascii_whitespace()).is_some() {}
520            ContainerId(String::from_utf8(stdout).into_diagnostic()?)
521        };
522        Ok(container_id)
523    }
524
525    fn remove_image_with_runner(runner: &RpmOstreeRunner, image_ref: &str) -> Result<()> {
526        trace!("PodmanDriver::remove_image_with_runner({runner:?}, {image_ref})");
527        let (cmd, args) = runner.command_args("podman", &["rmi"]);
528        let output = {
529            let c = cmd!(cmd, for args, image_ref);
530            trace!("{c:?}");
531            c
532        }
533        .output()
534        .into_diagnostic()?;
535
536        if !output.status.success() {
537            bail!("Failed to remove the image {image_ref}");
538        }
539
540        Ok(())
541    }
542}
543
544impl ContainerMountDriver for PodmanDriver {
545    fn mount_container(opts: ContainerOpts) -> Result<MountId> {
546        let output = {
547            let c = sudo_cmd!(
548                prompt = SUDO_PROMPT,
549                sudo_check = opts.privileged,
550                "podman",
551                "mount",
552                opts.container_id,
553            );
554            trace!("{c:?}");
555            c
556        }
557        .output()
558        .into_diagnostic()?;
559
560        if !output.status.success() {
561            bail!("Failed to mount container {}", opts.container_id);
562        }
563
564        Ok(MountId(
565            String::from_utf8(output.stdout.trim_ascii().to_vec()).into_diagnostic()?,
566        ))
567    }
568
569    fn unmount_container(opts: ContainerOpts) -> Result<()> {
570        let output = {
571            let c = sudo_cmd!(
572                prompt = SUDO_PROMPT,
573                sudo_check = opts.privileged,
574                "podman",
575                "unmount",
576                opts.container_id
577            );
578            trace!("{c:?}");
579            c
580        }
581        .output()
582        .into_diagnostic()?;
583
584        if !output.status.success() {
585            bail!("Failed to unmount container {}", opts.container_id);
586        }
587
588        Ok(())
589    }
590
591    fn remove_volume(opts: VolumeOpts) -> Result<()> {
592        let output = {
593            let c = sudo_cmd!(
594                prompt = SUDO_PROMPT,
595                sudo_check = opts.privileged,
596                "podman",
597                "volume",
598                "rm",
599                opts.volume_id
600            );
601            trace!("{c:?}");
602            c
603        }
604        .output()
605        .into_diagnostic()?;
606
607        if !output.status.success() {
608            bail!("Failed to remove volume {}", &opts.volume_id);
609        }
610
611        Ok(())
612    }
613}
614
615impl RechunkDriver for PodmanDriver {}
616
617impl RunDriver for PodmanDriver {
618    fn run(opts: RunOpts) -> Result<ExitStatus> {
619        trace!("PodmanDriver::run({opts:#?})");
620
621        let cid_path = TempDir::new().into_diagnostic()?;
622        let cid_file = cid_path.path().join("cid");
623
624        let cid = ContainerSignalId::new(&cid_file, ContainerRuntime::Podman, opts.privileged);
625
626        add_cid(&cid);
627
628        let status = podman_run(opts, &cid_file, false)
629            .build_status(opts.image, "Running container")
630            .into_diagnostic()?;
631
632        remove_cid(&cid);
633
634        Ok(status)
635    }
636
637    fn run_output(opts: RunOpts) -> Result<std::process::Output> {
638        trace!("PodmanDriver::run_output({opts:#?})");
639
640        let cid_path = TempDir::new().into_diagnostic()?;
641        let cid_file = cid_path.path().join("cid");
642
643        let cid = ContainerSignalId::new(&cid_file, ContainerRuntime::Podman, opts.privileged);
644
645        add_cid(&cid);
646
647        let output = podman_run(opts, &cid_file, false)
648            .output()
649            .into_diagnostic()?;
650
651        remove_cid(&cid);
652
653        Ok(output)
654    }
655
656    fn run_detached(opts: RunOpts) -> Result<DetachedContainer> {
657        trace!("PodmanDriver::run_detached({opts:#?})");
658
659        let cid_path = TempDir::new().into_diagnostic()?;
660        let cid_file = cid_path.path().join("cid");
661
662        let cid = ContainerSignalId::new(&cid_file, ContainerRuntime::Podman, opts.privileged);
663        let run_cmd = podman_run(opts, &cid_file, true);
664
665        DetachedContainer::start(cid, run_cmd)
666    }
667
668    fn create_container(opts: CreateContainerOpts) -> Result<ContainerId> {
669        trace!("PodmanDriver::create_container({opts:?})");
670
671        let output = {
672            let c = sudo_cmd!(
673                prompt = SUDO_PROMPT,
674                sudo_check = opts.privileged,
675                "podman",
676                "create",
677                opts.image.to_string(),
678                "bash"
679            );
680            trace!("{c:?}");
681            c
682        }
683        .output()
684        .into_diagnostic()?;
685
686        if !output.status.success() {
687            let err_out = String::from_utf8_lossy(&output.stderr);
688            bail!(
689                "Failed to create a container from image {}:\n{}",
690                opts.image,
691                err_out.trim()
692            );
693        }
694
695        Ok(ContainerId(
696            String::from_utf8(output.stdout.trim_ascii().to_vec()).into_diagnostic()?,
697        ))
698    }
699
700    fn remove_container(opts: RemoveContainerOpts) -> Result<()> {
701        trace!("PodmanDriver::remove_container({opts:?})");
702
703        let output = {
704            let c = sudo_cmd!(
705                prompt = SUDO_PROMPT,
706                sudo_check = opts.privileged,
707                "podman",
708                "rm",
709                opts.container_id,
710            );
711            trace!("{c:?}");
712            c
713        }
714        .output()
715        .into_diagnostic()?;
716
717        if !output.status.success() {
718            let err_out = String::from_utf8_lossy(&output.stderr);
719            bail!(
720                "Failed to remove container {}:\n{}",
721                opts.container_id,
722                err_out.trim()
723            );
724        }
725
726        Ok(())
727    }
728}
729
730impl ImageStorageDriver for PodmanDriver {
731    fn remove_image(opts: RemoveImageOpts) -> Result<()> {
732        trace!("PodmanDriver::remove_image({opts:?})");
733
734        let output = {
735            let c = sudo_cmd!(
736                prompt = SUDO_PROMPT,
737                sudo_check = opts.privileged,
738                "podman",
739                "rmi",
740                opts.image.to_string()
741            );
742            trace!("{c:?}");
743            c
744        }
745        .output()
746        .into_diagnostic()?;
747
748        if !output.status.success() {
749            let err_out = String::from_utf8_lossy(&output.stderr);
750            bail!(
751                "Failed to remove the image {}:\n{}",
752                opts.image,
753                err_out.trim()
754            );
755        }
756
757        Ok(())
758    }
759
760    fn list_images(privileged: bool) -> Result<Vec<Reference>> {
761        #[derive(Deserialize)]
762        #[serde(rename_all = "PascalCase")]
763        struct Image {
764            names: Option<Vec<String>>,
765        }
766
767        trace!("PodmanDriver::list_images({privileged})");
768
769        let output = {
770            let c = sudo_cmd!(
771                prompt = SUDO_PROMPT,
772                sudo_check = privileged,
773                "podman",
774                "images",
775                "--format",
776                "json"
777            );
778            trace!("{c:?}");
779            c
780        }
781        .output()
782        .into_diagnostic()?;
783
784        if !output.status.success() {
785            let err_out = String::from_utf8_lossy(&output.stderr);
786            bail!("Failed to list images:\n{}", err_out.trim());
787        }
788
789        let images: Vec<Image> = serde_json::from_slice(&output.stdout).into_diagnostic()?;
790
791        images
792            .into_iter()
793            .filter_map(|image| image.names)
794            .flat_map(|names| {
795                names
796                    .into_iter()
797                    .map(|name| name.parse::<Reference>().into_diagnostic())
798            })
799            .collect()
800    }
801}
802
803fn podman_run(opts: RunOpts, cid_file: &Path, detach: bool) -> Command {
804    let command = sudo_cmd!(
805        prompt = SUDO_PROMPT,
806        sudo_check = opts.privileged && !opts.rootless,
807        "podman",
808        "run",
809        format!("--cidfile={}", cid_file.display()),
810        if opts.privileged => [
811            "--privileged",
812            "--network=host",
813        ],
814        if opts.remove => "--rm",
815        if detach => "--detach",
816        if opts.pull => "--pull=always",
817        if let Some(user) = opts.user.as_ref() => format!("--user={user}"),
818        for RunOptsVolume { path_or_vol_name, container_path } in opts.volumes.iter() => [
819            "--volume",
820            format!("{path_or_vol_name}:{container_path}"),
821        ],
822        for RunOptsEnv { key, value } in opts.env_vars.iter() => [
823            "--env",
824            format!("{key}={value}"),
825        ],
826        opts.image,
827        for arg in opts.args.iter() => &**arg,
828    );
829    trace!("{command:?}");
830
831    command
832}