Skip to main content

blue_build_process_management/drivers/
traits.rs

1use std::{
2    borrow::Borrow,
3    ops::Not,
4    path::PathBuf,
5    process::{ExitStatus, Output},
6};
7
8use blue_build_utils::{
9    constants::COSIGN_PUB_PATH,
10    container::{ContainerId, ImageRef, MountId, OciRef, Tag},
11    platform::Platform,
12    retry,
13    semver::Version,
14    string_vec,
15};
16use comlexr::cmd;
17use log::{debug, info, trace, warn};
18use miette::{Context, IntoDiagnostic, Result, bail};
19use oci_client::Reference;
20use rayon::prelude::*;
21use semver::VersionReq;
22
23use super::{
24    Driver,
25    opts::{
26        BuildChunkedOciOpts, BuildOpts, BuildRechunkTagPushOpts, BuildTagPushOpts,
27        CheckKeyPairOpts, ContainerOpts, CopyOciOpts, CreateContainerOpts, GenerateImageNameOpts,
28        GenerateKeyPairOpts, GenerateTagsOpts, GetMetadataOpts, PruneOpts, PullOpts, PushOpts,
29        RechunkOpts, RemoveContainerOpts, RemoveImageOpts, RunOpts, SignOpts, SignVerifyOpts,
30        SwitchOpts, TagOpts, UntagOpts, VerifyOpts, VerifyType, VolumeOpts,
31    },
32    opts::{ManifestCreateOpts, ManifestPushOpts},
33    rpm_ostree_runner::RpmOstreeRunner,
34    types::CiDriverType,
35    types::{
36        BootDriverType, BuildDriverType, ImageMetadata, InspectDriverType, RunDriverType,
37        SigningDriverType,
38    },
39};
40use crate::{
41    drivers::opts::PrivateKey, logging::CommandLogging, signal_handler::DetachedContainer,
42};
43
44trait PrivateDriver {}
45
46macro_rules! impl_private_driver {
47    ($($driver:ty),* $(,)?) => {
48        $(
49            impl PrivateDriver for $driver {}
50        )*
51    };
52}
53
54impl_private_driver!(
55    super::Driver,
56    super::docker_driver::DockerDriver,
57    super::podman_driver::PodmanDriver,
58    super::buildah_driver::BuildahDriver,
59    super::github_driver::GithubDriver,
60    super::gitlab_driver::GitlabDriver,
61    super::local_driver::LocalDriver,
62    super::cosign_driver::CosignDriver,
63    super::skopeo_driver::SkopeoDriver,
64    super::sigstore_driver::SigstoreDriver,
65    super::rpm_ostree_driver::RpmOstreeDriver,
66    super::rpm_ostree_driver::Status,
67    super::rpm_ostree_runner::RpmOstreeContainer,
68    super::rpm_ostree_runner::RpmOstreeRunner,
69    super::oci_client_driver::OciClientDriver,
70    Option<BuildDriverType>,
71    Option<RunDriverType>,
72    Option<InspectDriverType>,
73    Option<SigningDriverType>,
74    Option<CiDriverType>,
75    Option<BootDriverType>,
76);
77
78#[cfg(feature = "bootc")]
79impl_private_driver!(
80    super::bootc_driver::BootcDriver,
81    super::bootc_driver::BootcStatus
82);
83
84#[expect(private_bounds)]
85pub trait DetermineDriver<T>: PrivateDriver {
86    fn determine_driver(&mut self) -> T;
87}
88
89/// Trait for retrieving version of a driver.
90#[expect(private_bounds)]
91pub trait DriverVersion: PrivateDriver {
92    /// The version req string slice that follows
93    /// the semver standard <https://semver.org/>.
94    const VERSION_REQ: &'static str;
95
96    /// Returns the version of the driver.
97    ///
98    /// # Errors
99    /// Will error if it can't retrieve the version.
100    fn version() -> Result<Version>;
101
102    #[must_use]
103    fn is_supported_version() -> bool {
104        Self::version().is_ok_and(|version| {
105            VersionReq::parse(Self::VERSION_REQ).is_ok_and(|req| req.matches(&version))
106        })
107    }
108}
109
110/// Allows agnostic building, tagging, pushing, and login.
111#[expect(private_bounds)]
112pub trait BuildDriver: PrivateDriver {
113    /// Runs the build logic for the driver.
114    ///
115    /// # Errors
116    /// Will error if the build fails.
117    fn build(opts: BuildOpts) -> Result<()>;
118
119    /// Runs the tag logic for the driver.
120    ///
121    /// # Errors
122    /// Will error if the tagging fails.
123    fn tag(opts: TagOpts) -> Result<()>;
124
125    /// Runs the untag logic for the driver.
126    ///
127    /// # Errors
128    /// Will error if the untagging fails.
129    fn untag(opts: UntagOpts) -> Result<()>;
130
131    /// Runs the push logic for the driver
132    ///
133    /// # Errors
134    /// Will error if the push fails.
135    fn push(opts: PushOpts) -> Result<()>;
136
137    /// Runs the pull logic for the driver
138    ///
139    /// # Errors
140    /// Will error if the pull fails.
141    fn pull(opts: PullOpts) -> Result<ContainerId>;
142
143    /// Runs the login logic for the driver.
144    ///
145    /// # Errors
146    /// Will error if login fails.
147    fn login(server: &str) -> Result<()>;
148
149    /// Runs prune commands for the driver.
150    ///
151    /// # Errors
152    /// Will error if the driver fails to prune.
153    fn prune(opts: super::opts::PruneOpts) -> Result<()>;
154
155    /// Create a manifest containing all the built images.
156    ///
157    /// # Errors
158    /// Will error if the driver fails to create a manifest.
159    fn manifest_create(opts: ManifestCreateOpts) -> Result<()>;
160
161    /// Pushes a manifest containing all the built images.
162    ///
163    /// # Errors
164    /// Will error if the driver fails to push a manifest.
165    fn manifest_push(opts: ManifestPushOpts) -> Result<()>;
166
167    /// Runs the logic for building, tagging, and pushing an image.
168    ///
169    /// # Errors
170    /// Will error if building, tagging, or pushing fails.
171    fn build_tag_push(opts: BuildTagPushOpts) -> Result<Vec<String>> {
172        trace!("BuildDriver::build_tag_push({opts:#?})");
173
174        assert!(
175            opts.platform.is_empty().not(),
176            "Must have at least 1 platform"
177        );
178        let platform_images: Vec<(ImageRef<'_>, Platform)> = opts
179            .platform
180            .iter()
181            .map(|&platform| (opts.image.with_platform(platform), platform))
182            .collect();
183
184        let build_opts = BuildOpts::builder()
185            .containerfile(opts.containerfile.as_ref())
186            .squash(opts.squash)
187            .maybe_cache_from(opts.cache_from)
188            .maybe_cache_to(opts.cache_to)
189            .secrets(opts.secrets);
190        let build_opts = platform_images
191            .iter()
192            .map(|(image, platform)| build_opts.clone().image(image).platform(*platform).build())
193            .collect::<Vec<_>>();
194
195        build_opts
196            .par_iter()
197            .try_for_each(|&build_opts| -> Result<()> {
198                info!("Building image {}", build_opts.image);
199
200                Self::build(build_opts)
201            })?;
202
203        let image_list: Vec<String> = match &opts.image {
204            ImageRef::Remote(image) if !opts.tags.is_empty() => {
205                debug!("Tagging all images");
206
207                let mut image_list = Vec::with_capacity(opts.tags.len());
208                let platform_images = opts
209                    .platform
210                    .iter()
211                    .map(|&platform| platform.tagged_image(image))
212                    .collect::<Vec<_>>();
213
214                for tag in opts.tags {
215                    debug!("Tagging {} with {tag}", &image);
216                    let tagged_image = Reference::with_tag(
217                        image.registry().into(),
218                        image.repository().into(),
219                        tag.to_string(),
220                    );
221
222                    Self::manifest_create(
223                        ManifestCreateOpts::builder()
224                            .final_image(&tagged_image)
225                            .image_list(&platform_images)
226                            .build(),
227                    )?;
228                    image_list.push(tagged_image.to_string());
229
230                    if opts.push {
231                        let retry_count = if opts.retry_push { opts.retry_count } else { 0 };
232
233                        // Push images with retries (1s delay between retries)
234                        blue_build_utils::retry(retry_count, 5, || {
235                            debug!("Pushing image {tagged_image}");
236
237                            Self::manifest_push(
238                                ManifestPushOpts::builder()
239                                    .final_image(&tagged_image)
240                                    .compression_type(opts.compression)
241                                    .build(),
242                            )
243                        })?;
244                    }
245                }
246
247                image_list
248            }
249            _ => {
250                string_vec![opts.image]
251            }
252        };
253
254        Ok(image_list)
255    }
256}
257
258/// Allows agnostic inspection of images.
259#[expect(private_bounds)]
260pub trait InspectDriver: PrivateDriver {
261    /// Gets the metadata on an image tag.
262    ///
263    /// # Errors
264    /// Will error if it is unable to get the labels.
265    fn get_metadata(opts: GetMetadataOpts) -> Result<ImageMetadata>;
266}
267
268/// Allows agnostic running of containers.
269pub trait RunDriver: ImageStorageDriver {
270    /// Run a container to perform an action.
271    ///
272    /// # Errors
273    /// Will error if there is an issue running the container.
274    fn run(opts: RunOpts) -> Result<ExitStatus>;
275
276    /// Run a container to perform an action and capturing output.
277    ///
278    /// # Errors
279    /// Will error if there is an issue running the container.
280    fn run_output(opts: RunOpts) -> Result<Output>;
281
282    /// Run a container to perform an action in the background.
283    /// The container will be stopped when the returned `DetachedContainer`
284    /// value is dropped.
285    ///
286    /// # Errors
287    /// Will error if there is an issue running the container.
288    fn run_detached(opts: RunOpts) -> Result<DetachedContainer>;
289
290    /// Creates container
291    ///
292    /// # Errors
293    /// Will error if the container create command fails.
294    fn create_container(opts: CreateContainerOpts) -> Result<ContainerId>;
295
296    /// Removes a container
297    ///
298    /// # Errors
299    /// Will error if the container remove command fails.
300    fn remove_container(opts: RemoveContainerOpts) -> Result<()>;
301}
302
303/// Allows agnostic management of container image storage.
304#[expect(private_bounds)]
305pub trait ImageStorageDriver: PrivateDriver {
306    /// Removes an image
307    ///
308    /// # Errors
309    /// Will error if the image remove command fails.
310    fn remove_image(opts: RemoveImageOpts) -> Result<()>;
311
312    /// List all images in the local image registry.
313    ///
314    /// # Errors
315    /// Will error if the image list command fails.
316    fn list_images(privileged: bool) -> Result<Vec<Reference>>;
317}
318
319pub trait BuildChunkedOciDriver: BuildDriver + ImageStorageDriver {
320    /// Create a manifest containing all the built images.
321    /// Runs within the same context as rpm-ostree.
322    ///
323    /// # Errors
324    /// Will error if the driver fails to create a manifest.
325    fn manifest_create_with_runner(
326        runner: &RpmOstreeRunner,
327        opts: ManifestCreateOpts,
328    ) -> Result<()>;
329
330    /// Pushes a manifest containing all the built images.
331    /// Runs within the same context as rpm-ostree.
332    ///
333    /// # Errors
334    /// Will error if the driver fails to push a manifest.
335    fn manifest_push_with_runner(runner: &RpmOstreeRunner, opts: ManifestPushOpts) -> Result<()>;
336
337    /// Pull an image from a remote registry.
338    /// Runs within the same context as rpm-ostree.
339    ///
340    /// # Errors
341    /// Will error if the driver fails to pull the image.
342    fn pull_with_runner(runner: &RpmOstreeRunner, opts: PullOpts) -> Result<ContainerId>;
343
344    /// Removes an image from local storage.
345    /// Runs within the same context as rpm-ostree.
346    ///
347    /// # Errors
348    /// Will error if image removal fails.
349    fn remove_image_with_runner(runner: &RpmOstreeRunner, image_ref: &str) -> Result<()>;
350
351    /// Runs build-chunked-oci on an image.
352    ///
353    /// # Errors
354    /// Will error if rechunking fails.
355    fn build_chunked_oci(
356        runner: &RpmOstreeRunner,
357        unchunked_image: &ImageRef<'_>,
358        final_image: &ImageRef<'_>,
359        opts: BuildChunkedOciOpts,
360    ) -> Result<()> {
361        trace!(
362            concat!(
363                "BuildChunkedOciDriver::build_chunked_oci(\n",
364                "runner: {:#?},\n",
365                "unchunked_image: {},\n",
366                "final_image: {},\n",
367                "opts: {:#?})\n)"
368            ),
369            runner, unchunked_image, final_image, opts,
370        );
371
372        let prev_image_id = if !opts.clear_plan
373            && let ImageRef::Remote(image_ref) = final_image
374        {
375            Self::pull_with_runner(
376                runner,
377                PullOpts::builder()
378                    .image(image_ref)
379                    .maybe_platform(opts.platform)
380                    .retry_count(5)
381                    .build(),
382            )
383            .inspect_err(|_| {
384                warn!("Failed to pull previous build; rechunking will use fresh layer plan.");
385            })
386            .ok()
387        } else {
388            None
389        };
390
391        let (first_cmd, args) =
392            runner.command_args("rpm-ostree", &["compose", "build-chunked-oci"]);
393        let transport_ref = match final_image {
394            ImageRef::Remote(image) => format!("containers-storage:{image}"),
395            _ => final_image.to_string(),
396        };
397        let command = cmd!(
398            first_cmd,
399            for args,
400            "--bootc",
401            format!("--format-version={}", opts.format_version),
402            format!("--max-layers={}", opts.max_layers),
403            format!("--from={unchunked_image}"),
404            format!("--output={transport_ref}"),
405        );
406        trace!("{command:?}");
407        let status = command
408            .build_status(final_image.to_string(), "Rechunking image")
409            .into_diagnostic()?;
410
411        if let Some(image_id) = prev_image_id {
412            Self::remove_image_with_runner(runner, &image_id.0)?;
413        }
414
415        if !status.success() {
416            bail!("Failed to rechunk image {}", final_image);
417        }
418
419        Ok(())
420    }
421
422    /// Runs the logic for building, rechunking, tagging, and pushing an image.
423    ///
424    /// # Errors
425    /// Will error if building, rechunking, tagging, or pushing fails.
426    #[expect(clippy::too_many_lines)]
427    fn build_rechunk_tag_push(opts: BuildRechunkTagPushOpts) -> Result<Vec<String>> {
428        trace!("BuildChunkedOciDriver::build_rechunk_tag_push({opts:#?})");
429
430        let BuildRechunkTagPushOpts {
431            build_tag_push_opts: btp_opts,
432            rechunk_opts,
433            remove_base_image,
434        } = opts;
435
436        assert!(
437            btp_opts.platform.is_empty().not(),
438            "Must have at least 1 platform"
439        );
440        let build_opts = BuildOpts::builder()
441            .containerfile(btp_opts.containerfile.as_ref())
442            .squash(true)
443            .secrets(btp_opts.secrets);
444
445        let images_to_rechunk: Vec<(ImageRef, ImageRef, Platform)> = btp_opts
446            .platform
447            .par_iter()
448            .map(|&platform| -> Result<(ImageRef, ImageRef, Platform)> {
449                let image = btp_opts.image.with_platform(platform);
450                let unchunked_image =
451                    image.append_tag(&"unchunked".parse().expect("Should be a valid tag"));
452                info!("Building image {image}");
453
454                Self::build(
455                    build_opts
456                        .clone()
457                        .image(&unchunked_image)
458                        .platform(platform)
459                        .build(),
460                )?;
461                Ok((unchunked_image, image, platform))
462            })
463            .collect::<Result<Vec<_>>>()?;
464
465        if let Some(base_image) = remove_base_image {
466            Self::remove_image(
467                RemoveImageOpts::builder()
468                    .image(base_image)
469                    .privileged(btp_opts.privileged)
470                    .build(),
471            )?;
472            Self::prune(PruneOpts::builder().volumes(true).build())?;
473        }
474
475        // Run subsequent commands on host if rpm-ostree is available on host, otherwise
476        // run in container that has rpm-ostree installed.
477        let runner = RpmOstreeRunner::start()?;
478
479        // Rechunk images serially to avoid using excessive disk space.
480        if let ImageRef::Remote(image_ref) = btp_opts.image {
481            for (unchunked_image, image, platform) in images_to_rechunk {
482                // Use the non-platform-tagged image ref as the output for build-chunked-oci
483                // so it looks for an existing manifest at the right location (the multi-arch
484                // image that will be pushed). This allows build-chunked-oci to read the
485                // previous build's layer annotations to minimize layout changes.
486                let result = Self::build_chunked_oci(
487                    &runner,
488                    &unchunked_image,
489                    btp_opts.image,
490                    rechunk_opts.with_platform(platform),
491                );
492                // Clean up the unchunked image whether or not rechunking succeeded.
493                if let ImageRef::Remote(unchunked_image) = unchunked_image {
494                    Self::remove_image(RemoveImageOpts::builder().image(&unchunked_image).build())?;
495                }
496                result?;
497
498                // Now retag the image to use the platform tag.
499                if let ImageRef::Remote(image_with_platform) = image {
500                    Self::tag(
501                        TagOpts::builder()
502                            .src_image(image_ref)
503                            .dest_image(&image_with_platform)
504                            .privileged(btp_opts.privileged)
505                            .build(),
506                    )?;
507                    Self::untag(
508                        UntagOpts::builder()
509                            .image(image_ref)
510                            .privileged(btp_opts.privileged)
511                            .build(),
512                    )?;
513                }
514            }
515        } else {
516            for (unchunked_image, image, platform) in images_to_rechunk {
517                Self::build_chunked_oci(
518                    &runner,
519                    &unchunked_image,
520                    &image,
521                    rechunk_opts.with_platform(platform),
522                )?;
523            }
524        }
525
526        let image_list: Vec<String> = match &btp_opts.image {
527            ImageRef::Remote(image) if !btp_opts.tags.is_empty() => {
528                debug!("Tagging all images");
529
530                let mut image_list = Vec::with_capacity(btp_opts.tags.len());
531                let platform_images = btp_opts
532                    .platform
533                    .iter()
534                    .map(|&platform| platform.tagged_image(image))
535                    .collect::<Vec<_>>();
536
537                for tag in btp_opts.tags {
538                    debug!("Tagging {} with {tag}", &image);
539                    let tagged_image = Reference::with_tag(
540                        image.registry().into(),
541                        image.repository().into(),
542                        tag.to_string(),
543                    );
544
545                    Self::manifest_create_with_runner(
546                        &runner,
547                        ManifestCreateOpts::builder()
548                            .final_image(&tagged_image)
549                            .image_list(&platform_images)
550                            .build(),
551                    )?;
552                    image_list.push(tagged_image.to_string());
553
554                    if btp_opts.push {
555                        let retry_count = if btp_opts.retry_push {
556                            btp_opts.retry_count
557                        } else {
558                            0
559                        };
560
561                        // Push images with retries (1s delay between retries)
562                        blue_build_utils::retry(retry_count, 5, || {
563                            debug!("Pushing image {tagged_image}");
564
565                            // We push twice due to a (very strange) bug in podman where layer
566                            // annotations aren't pushed unless the layer already exists in the
567                            // remote registry. See:
568                            // https://github.com/containers/podman/issues/27796
569                            Self::manifest_push_with_runner(
570                                &runner,
571                                ManifestPushOpts::builder()
572                                    .final_image(&tagged_image)
573                                    .compression_type(btp_opts.compression)
574                                    .build(),
575                            )
576                            .and_then(|()| {
577                                Self::manifest_push_with_runner(
578                                    &runner,
579                                    ManifestPushOpts::builder()
580                                        .final_image(&tagged_image)
581                                        .compression_type(btp_opts.compression)
582                                        .build(),
583                                )
584                            })
585                        })?;
586                    }
587                }
588
589                image_list
590            }
591            _ => {
592                string_vec![btp_opts.image]
593            }
594        };
595
596        Ok(image_list)
597    }
598}
599
600#[expect(private_bounds)]
601pub(super) trait ContainerMountDriver: PrivateDriver {
602    /// Mounts the container
603    ///
604    /// # Errors
605    /// Will error if the container mount command fails.
606    fn mount_container(opts: ContainerOpts) -> Result<MountId>;
607
608    /// Unmount the container
609    ///
610    /// # Errors
611    /// Will error if the container unmount command fails.
612    fn unmount_container(opts: ContainerOpts) -> Result<()>;
613
614    /// Remove a volume
615    ///
616    /// # Errors
617    /// Will error if the volume remove command fails.
618    fn remove_volume(opts: VolumeOpts) -> Result<()>;
619}
620
621#[expect(private_bounds)]
622pub trait OciCopy: PrivateDriver {
623    /// Copy an OCI image.
624    ///
625    /// # Errors
626    /// Will error if copying the image fails.
627    fn copy_oci(&self, opts: CopyOciOpts) -> Result<()>;
628}
629
630#[expect(private_bounds)]
631pub trait RechunkDriver: RunDriver + BuildDriver + ContainerMountDriver {
632    const RECHUNK_IMAGE: &str = "ghcr.io/hhd-dev/rechunk:v1.0.1";
633
634    /// Perform a rechunk build of a recipe.
635    ///
636    /// # Errors
637    /// Will error if the rechunk process fails.
638    fn rechunk(opts: RechunkOpts) -> Result<Vec<String>> {
639        assert!(
640            opts.platform.is_empty().not(),
641            "Must have at least one platform defined!"
642        );
643
644        let temp_dir = if let Some(dir) = opts.tempdir {
645            &tempfile::TempDir::new_in(dir).into_diagnostic()?
646        } else {
647            &tempfile::TempDir::new().into_diagnostic()?
648        };
649        let ostree_cache_id = &uuid::Uuid::new_v4().to_string();
650        let image = &ImageRef::from(
651            Reference::try_from(format!("localhost/{ostree_cache_id}/raw-rechunk")).unwrap(),
652        );
653        let current_dir = &std::env::current_dir().into_diagnostic()?;
654        let current_dir = &*current_dir.to_string_lossy();
655        let main_tag = opts.tags.first().cloned().unwrap_or_default();
656        let final_image = Reference::with_tag(
657            opts.image.resolve_registry().into(),
658            opts.image.repository().into(),
659            main_tag.as_str().into(),
660        );
661
662        Self::login(final_image.registry())?;
663
664        let platform_images = opts
665            .platform
666            .iter()
667            .map(|&platform| (image.with_platform(platform), platform))
668            .collect::<Vec<_>>();
669        let build_opts = platform_images
670            .iter()
671            .map(|(image, platform)| {
672                BuildOpts::builder()
673                    .image(image)
674                    .containerfile(opts.containerfile)
675                    .platform(*platform)
676                    .privileged(true)
677                    .squash(true)
678                    .host_network(true)
679                    .secrets(opts.secrets)
680                    .build()
681            })
682            .collect::<Vec<_>>();
683
684        build_opts.par_iter().try_for_each(|&build_opts| {
685            let ImageRef::Remote(image) = build_opts.image else {
686                bail!("Cannot build for {}", build_opts.image);
687            };
688            Self::build(build_opts)?;
689            let container = &Self::create_container(
690                CreateContainerOpts::builder()
691                    .image(image)
692                    .privileged(true)
693                    .build(),
694            )?;
695            let mount = &Self::mount_container(
696                ContainerOpts::builder()
697                    .container_id(container)
698                    .privileged(true)
699                    .build(),
700            )?;
701
702            Self::prune_image(mount, container, image, opts)?;
703            Self::create_ostree_commit(mount, ostree_cache_id, container, image, opts)?;
704
705            let temp_dir_str = &*temp_dir.path().to_string_lossy();
706
707            Self::rechunk_image(ostree_cache_id, temp_dir_str, current_dir, opts)
708        })?;
709
710        let mut image_list = Vec::with_capacity(opts.tags.len());
711
712        if opts.push {
713            let oci_dir = OciRef::from_oci_directory(temp_dir.path().join(ostree_cache_id))?;
714
715            for tag in opts.tags {
716                let tagged_image = Reference::with_tag(
717                    final_image.registry().to_string(),
718                    final_image.repository().to_string(),
719                    tag.to_string(),
720                );
721
722                blue_build_utils::retry(opts.retry_count, 5, || {
723                    debug!("Pushing image {tagged_image}");
724
725                    Driver.copy_oci(
726                        CopyOciOpts::builder()
727                            .src_ref(&oci_dir)
728                            .dest_ref(&OciRef::from_remote_ref(&tagged_image))
729                            .privileged(true)
730                            .build(),
731                    )
732                })?;
733                image_list.push(tagged_image.into());
734            }
735        }
736
737        Ok(image_list)
738    }
739
740    /// Step 1 of the rechunk process that prunes excess files.
741    ///
742    /// # Errors
743    /// Will error if the prune process fails.
744    fn prune_image(
745        mount: &MountId,
746        container: &ContainerId,
747        image: &Reference,
748        opts: RechunkOpts<'_>,
749    ) -> Result<(), miette::Error> {
750        let status = Self::run(
751            RunOpts::builder()
752                .image(Self::RECHUNK_IMAGE)
753                .remove(true)
754                .user("0:0")
755                .privileged(true)
756                .volumes(&crate::run_volumes! {
757                    mount => "/var/tree",
758                })
759                .env_vars(&crate::run_envs! {
760                    "TREE" => "/var/tree",
761                })
762                .args(&bon::vec!["/sources/rechunk/1_prune.sh"])
763                .build(),
764        )?;
765
766        if !status.success() {
767            Self::unmount_container(
768                super::opts::ContainerOpts::builder()
769                    .container_id(container)
770                    .privileged(true)
771                    .build(),
772            )?;
773            Self::remove_container(
774                RemoveContainerOpts::builder()
775                    .container_id(container)
776                    .privileged(true)
777                    .build(),
778            )?;
779            Self::remove_image(
780                RemoveImageOpts::builder()
781                    .image(image)
782                    .privileged(true)
783                    .build(),
784            )?;
785            bail!("Failed to run prune step for {}", &opts.image);
786        }
787
788        Ok(())
789    }
790
791    /// Step 2 of the rechunk process that creates the ostree commit.
792    ///
793    /// # Errors
794    /// Will error if the ostree commit process fails.
795    fn create_ostree_commit(
796        mount: &MountId,
797        ostree_cache_id: &str,
798        container: &ContainerId,
799        image: &Reference,
800        opts: RechunkOpts<'_>,
801    ) -> Result<()> {
802        let status = Self::run(
803            RunOpts::builder()
804                .image(Self::RECHUNK_IMAGE)
805                .remove(true)
806                .user("0:0")
807                .privileged(true)
808                .volumes(&crate::run_volumes! {
809                    mount => "/var/tree",
810                    ostree_cache_id => "/var/ostree",
811                })
812                .env_vars(&crate::run_envs! {
813                    "TREE" => "/var/tree",
814                    "REPO" => "/var/ostree/repo",
815                    "RESET_TIMESTAMP" => "1",
816                })
817                .args(&bon::vec!["/sources/rechunk/2_create.sh"])
818                .build(),
819        )?;
820        Self::unmount_container(
821            super::opts::ContainerOpts::builder()
822                .container_id(container)
823                .privileged(true)
824                .build(),
825        )?;
826        Self::remove_container(
827            RemoveContainerOpts::builder()
828                .container_id(container)
829                .privileged(true)
830                .build(),
831        )?;
832        Self::remove_image(
833            RemoveImageOpts::builder()
834                .image(image)
835                .privileged(true)
836                .build(),
837        )?;
838
839        if !status.success() {
840            bail!("Failed to run Ostree create step for {}", &opts.image);
841        }
842
843        Ok(())
844    }
845
846    /// Step 3 of the rechunk process that generates the final chunked image.
847    ///
848    /// # Errors
849    /// Will error if the chunk process fails.
850    fn rechunk_image(
851        ostree_cache_id: &str,
852        temp_dir_str: &str,
853        current_dir: &str,
854        opts: RechunkOpts<'_>,
855    ) -> Result<()> {
856        let out_ref = format!("oci:{ostree_cache_id}");
857        let image = opts.image.to_string();
858        let label_string = opts
859            .labels
860            .iter()
861            .map(|(k, v)| format!("{k}={v}"))
862            .reduce(|a, b| format!("{a}\n{b}"))
863            .unwrap_or_default();
864        let status = Self::run(
865            RunOpts::builder()
866                .image(Self::RECHUNK_IMAGE)
867                .remove(true)
868                .user("0:0")
869                .privileged(true)
870                .volumes(&crate::run_volumes! {
871                    ostree_cache_id => "/var/ostree",
872                    temp_dir_str => "/workspace",
873                    current_dir => "/var/git"
874                })
875                .env_vars(&crate::run_envs! {
876                    "REPO" => "/var/ostree/repo",
877                    "PREV_REF" => &image,
878                    "OUT_NAME" => ostree_cache_id,
879                    "CLEAR_PLAN" => if opts.clear_plan { "true" } else { "" },
880                    "VERSION" => opts.version,
881                    "OUT_REF" => &out_ref,
882                    "GIT_DIR" => "/var/git",
883                    "LABELS" => &label_string,
884                })
885                .args(&bon::vec!["/sources/rechunk/3_chunk.sh"])
886                .build(),
887        )?;
888
889        Self::remove_volume(
890            super::opts::VolumeOpts::builder()
891                .volume_id(ostree_cache_id)
892                .privileged(true)
893                .build(),
894        )?;
895
896        if !status.success() {
897            bail!("Failed to run rechunking for {}", &opts.image);
898        }
899
900        Ok(())
901    }
902}
903
904/// Allows agnostic management of signature keys.
905#[expect(private_bounds)]
906pub trait SigningDriver: PrivateDriver {
907    /// Generate a new private/public key pair.
908    ///
909    /// # Errors
910    /// Will error if a key-pair couldn't be generated.
911    fn generate_key_pair(opts: GenerateKeyPairOpts) -> Result<()>;
912
913    /// Checks the signing key files to ensure
914    /// they match.
915    ///
916    /// # Errors
917    /// Will error if the files cannot be verified.
918    fn check_signing_files(opts: CheckKeyPairOpts) -> Result<()>;
919
920    /// Signs the image digest.
921    ///
922    /// # Errors
923    /// Will error if signing fails.
924    fn sign(opts: SignOpts) -> Result<()>;
925
926    /// Verifies the image.
927    ///
928    /// The image can be verified either with `VerifyType::File` containing
929    /// the public key contents, or with `VerifyType::Keyless` containing
930    /// information about the `issuer` and `identity`.
931    ///
932    /// # Errors
933    /// Will error if the image fails to be verified.
934    fn verify(opts: VerifyOpts) -> Result<()>;
935
936    /// Sign an image given the image name and tag.
937    ///
938    /// # Errors
939    /// Will error if the image fails to be signed.
940    fn sign_and_verify(opts: SignVerifyOpts) -> Result<()> {
941        trace!("sign_and_verify({opts:?})");
942
943        let path = opts
944            .dir
945            .as_ref()
946            .map_or_else(|| PathBuf::from("."), |d| d.to_path_buf());
947        let cosign_file_path = path.join(COSIGN_PUB_PATH);
948
949        let metadata = Driver::get_metadata(
950            GetMetadataOpts::builder()
951                .image(opts.image)
952                .no_cache(true)
953                .build(),
954        )?;
955        let image_digest = Reference::with_digest(
956            opts.image.resolve_registry().into(),
957            opts.image.repository().into(),
958            metadata.digest().into(),
959        );
960        let issuer = Driver::oidc_provider();
961        let identity = Driver::keyless_cert_identity();
962        let priv_key = PrivateKey::new(&path);
963
964        let (sign_opts, verify_opts) =
965            match (Driver::get_ci_driver(), &priv_key, &issuer, &identity) {
966                // Cosign public/private key pair
967                (_, Ok(priv_key), _, _) => (
968                    SignOpts::builder()
969                        .image(&image_digest)
970                        .key(priv_key)
971                        .metadata(&metadata)
972                        .build(),
973                    VerifyOpts::builder()
974                        .image(&image_digest)
975                        .verify_type(VerifyType::File(&cosign_file_path))
976                        .build(),
977                ),
978                // Gitlab keyless
979                (CiDriverType::Github | CiDriverType::Gitlab, _, Ok(issuer), Ok(identity)) => (
980                    SignOpts::builder()
981                        .metadata(&metadata)
982                        .image(&image_digest)
983                        .build(),
984                    VerifyOpts::builder()
985                        .image(&image_digest)
986                        .verify_type(VerifyType::Keyless { issuer, identity })
987                        .build(),
988                ),
989                _ => bail!("Failed to get information for signing the image"),
990            };
991
992        let retry_count = if opts.retry_push { opts.retry_count } else { 0 };
993
994        retry(retry_count, 5, || {
995            Self::sign(sign_opts)?;
996            Self::verify(verify_opts)
997        })?;
998
999        Ok(())
1000    }
1001
1002    /// Runs the login logic for the signing driver.
1003    ///
1004    /// # Errors
1005    /// Will error if login fails.
1006    fn signing_login(server: &str) -> Result<()>;
1007}
1008
1009/// Allows agnostic retrieval of CI-based information.
1010#[expect(private_bounds)]
1011pub trait CiDriver: PrivateDriver {
1012    /// Determines if we're on the main branch of
1013    /// a repository.
1014    fn on_default_branch() -> bool;
1015
1016    /// Retrieve the certificate identity for
1017    /// keyless signing.
1018    ///
1019    /// # Errors
1020    /// Will error if the environment variables aren't set.
1021    fn keyless_cert_identity() -> Result<String>;
1022
1023    /// Retrieve the OIDC Provider for keyless signing.
1024    ///
1025    /// # Errors
1026    /// Will error if the environment variables aren't set.
1027    fn oidc_provider() -> Result<String>;
1028
1029    /// Generate a list of tags based on the OS version.
1030    ///
1031    /// ## CI
1032    /// The tags are generated based on the CI system that
1033    /// is detected. The general format for the default branch is:
1034    /// - `${os_version}`
1035    /// - `${timestamp}-${os_version}`
1036    ///
1037    /// On a branch:
1038    /// - `br-${branch_name}-${os_version}`
1039    ///
1040    /// In a PR(GitHub)/MR(GitLab)
1041    /// - `pr-${pr_event_number}-${os_version}`/`mr-${mr_iid}-${os_version}`
1042    ///
1043    /// In all above cases the short git sha is also added:
1044    /// - `${commit_sha}-${os_version}`
1045    ///
1046    /// When `alt_tags` are not present, the following tags are added:
1047    /// - `latest`
1048    /// - `${timestamp}`
1049    ///
1050    /// ## Locally
1051    /// When ran locally, only a local tag is created:
1052    /// - `local-${os_version}`
1053    ///
1054    /// # Errors
1055    /// Will error if the environment variables aren't set.
1056    fn generate_tags(opts: GenerateTagsOpts) -> Result<Vec<Tag>>;
1057
1058    /// Generates the image name based on CI.
1059    ///
1060    /// # Errors
1061    /// Will error if the environment variables aren't set.
1062    fn generate_image_name<'a, O>(opts: O) -> Result<Reference>
1063    where
1064        O: Borrow<GenerateImageNameOpts<'a>>,
1065    {
1066        fn inner(opts: &GenerateImageNameOpts, driver_registry: &str) -> Result<Reference> {
1067            let image = match (opts.registry, opts.registry_namespace, opts.tag) {
1068                (Some(registry), Some(registry_namespace), Some(tag)) => {
1069                    format!(
1070                        "{}/{}/{}:{}",
1071                        registry.trim().to_lowercase(),
1072                        registry_namespace.trim().to_lowercase(),
1073                        opts.name.trim().to_lowercase(),
1074                        tag,
1075                    )
1076                }
1077                (Some(registry), Some(registry_namespace), None) => {
1078                    format!(
1079                        "{}/{}/{}",
1080                        registry.trim().to_lowercase(),
1081                        registry_namespace.trim().to_lowercase(),
1082                        opts.name.trim().to_lowercase(),
1083                    )
1084                }
1085                (Some(registry), None, None) => {
1086                    format!(
1087                        "{}/{}",
1088                        registry.trim().to_lowercase(),
1089                        opts.name.trim().to_lowercase(),
1090                    )
1091                }
1092                (Some(registry), None, Some(tag)) => {
1093                    format!(
1094                        "{}/{}:{}",
1095                        registry.trim().to_lowercase(),
1096                        opts.name.trim().to_lowercase(),
1097                        tag,
1098                    )
1099                }
1100                (None, Some(namespace), None) => {
1101                    format!(
1102                        "{}/{}/{}",
1103                        driver_registry.trim().to_lowercase(),
1104                        namespace.trim().to_lowercase(),
1105                        opts.name.trim().to_lowercase()
1106                    )
1107                }
1108                (None, Some(namespace), Some(tag)) => {
1109                    format!(
1110                        "{}/{}/{}:{}",
1111                        driver_registry.trim().to_lowercase(),
1112                        namespace.trim().to_lowercase(),
1113                        opts.name.trim().to_lowercase(),
1114                        tag,
1115                    )
1116                }
1117                (None, None, Some(tag)) => {
1118                    format!(
1119                        "{}/{}:{}",
1120                        driver_registry.trim().to_lowercase(),
1121                        opts.name.trim().to_lowercase(),
1122                        tag,
1123                    )
1124                }
1125                (None, None, None) => {
1126                    format!(
1127                        "{}/{}",
1128                        driver_registry.trim().to_lowercase(),
1129                        opts.name.trim().to_lowercase(),
1130                    )
1131                }
1132            };
1133            image
1134                .parse()
1135                .into_diagnostic()
1136                .with_context(|| format!("Unable to parse image {image}"))
1137        }
1138        inner(opts.borrow(), &Self::get_registry()?)
1139    }
1140
1141    /// Get the URL for the repository.
1142    ///
1143    /// # Errors
1144    /// Will error if the environment variables aren't set.
1145    fn get_repo_url() -> Result<String>;
1146
1147    /// Get the registry ref for the image.
1148    ///
1149    /// # Errors
1150    /// Will error if the environment variables aren't set.
1151    fn get_registry() -> Result<String>;
1152
1153    fn default_ci_file_path() -> PathBuf;
1154}
1155
1156#[expect(private_bounds)]
1157pub trait BootDriver: PrivateDriver {
1158    /// Get the status of the current booted image.
1159    ///
1160    /// # Errors
1161    /// Will error if we fail to get the status.
1162    fn status() -> Result<Box<dyn BootStatus>>;
1163
1164    /// Switch to a new image.
1165    ///
1166    /// # Errors
1167    /// Will error if we fail to switch to a new image.
1168    fn switch(opts: SwitchOpts) -> Result<()>;
1169
1170    /// Upgrade an image.
1171    ///
1172    /// # Errors
1173    /// Will error if we fail to upgrade to a new image.
1174    fn upgrade(opts: SwitchOpts) -> Result<()>;
1175}
1176
1177#[expect(private_bounds)]
1178pub trait BootStatus: PrivateDriver {
1179    /// Checks to see if there's a transaction in progress.
1180    fn transaction_in_progress(&self) -> bool;
1181
1182    /// Gets the booted image.
1183    fn booted_image(&self) -> Option<ImageRef<'_>>;
1184
1185    /// Gets the staged image.
1186    fn staged_image(&self) -> Option<ImageRef<'_>>;
1187}