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 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 .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 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, &ref_string, );
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}