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#[expect(private_bounds)]
91pub trait DriverVersion: PrivateDriver {
92 const VERSION_REQ: &'static str;
95
96 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#[expect(private_bounds)]
112pub trait BuildDriver: PrivateDriver {
113 fn build(opts: BuildOpts) -> Result<()>;
118
119 fn tag(opts: TagOpts) -> Result<()>;
124
125 fn untag(opts: UntagOpts) -> Result<()>;
130
131 fn push(opts: PushOpts) -> Result<()>;
136
137 fn pull(opts: PullOpts) -> Result<ContainerId>;
142
143 fn login(server: &str) -> Result<()>;
148
149 fn prune(opts: super::opts::PruneOpts) -> Result<()>;
154
155 fn manifest_create(opts: ManifestCreateOpts) -> Result<()>;
160
161 fn manifest_push(opts: ManifestPushOpts) -> Result<()>;
166
167 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 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#[expect(private_bounds)]
260pub trait InspectDriver: PrivateDriver {
261 fn get_metadata(opts: GetMetadataOpts) -> Result<ImageMetadata>;
266}
267
268pub trait RunDriver: ImageStorageDriver {
270 fn run(opts: RunOpts) -> Result<ExitStatus>;
275
276 fn run_output(opts: RunOpts) -> Result<Output>;
281
282 fn run_detached(opts: RunOpts) -> Result<DetachedContainer>;
289
290 fn create_container(opts: CreateContainerOpts) -> Result<ContainerId>;
295
296 fn remove_container(opts: RemoveContainerOpts) -> Result<()>;
301}
302
303#[expect(private_bounds)]
305pub trait ImageStorageDriver: PrivateDriver {
306 fn remove_image(opts: RemoveImageOpts) -> Result<()>;
311
312 fn list_images(privileged: bool) -> Result<Vec<Reference>>;
317}
318
319pub trait BuildChunkedOciDriver: BuildDriver + ImageStorageDriver {
320 fn manifest_create_with_runner(
326 runner: &RpmOstreeRunner,
327 opts: ManifestCreateOpts,
328 ) -> Result<()>;
329
330 fn manifest_push_with_runner(runner: &RpmOstreeRunner, opts: ManifestPushOpts) -> Result<()>;
336
337 fn pull_with_runner(runner: &RpmOstreeRunner, opts: PullOpts) -> Result<ContainerId>;
343
344 fn remove_image_with_runner(runner: &RpmOstreeRunner, image_ref: &str) -> Result<()>;
350
351 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 #[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 let runner = RpmOstreeRunner::start()?;
478
479 if let ImageRef::Remote(image_ref) = btp_opts.image {
481 for (unchunked_image, image, platform) in images_to_rechunk {
482 let result = Self::build_chunked_oci(
487 &runner,
488 &unchunked_image,
489 btp_opts.image,
490 rechunk_opts.with_platform(platform),
491 );
492 if let ImageRef::Remote(unchunked_image) = unchunked_image {
494 Self::remove_image(RemoveImageOpts::builder().image(&unchunked_image).build())?;
495 }
496 result?;
497
498 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 blue_build_utils::retry(retry_count, 5, || {
563 debug!("Pushing image {tagged_image}");
564
565 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 fn mount_container(opts: ContainerOpts) -> Result<MountId>;
607
608 fn unmount_container(opts: ContainerOpts) -> Result<()>;
613
614 fn remove_volume(opts: VolumeOpts) -> Result<()>;
619}
620
621#[expect(private_bounds)]
622pub trait OciCopy: PrivateDriver {
623 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 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 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 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 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#[expect(private_bounds)]
906pub trait SigningDriver: PrivateDriver {
907 fn generate_key_pair(opts: GenerateKeyPairOpts) -> Result<()>;
912
913 fn check_signing_files(opts: CheckKeyPairOpts) -> Result<()>;
919
920 fn sign(opts: SignOpts) -> Result<()>;
925
926 fn verify(opts: VerifyOpts) -> Result<()>;
935
936 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 (_, 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 (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 fn signing_login(server: &str) -> Result<()>;
1007}
1008
1009#[expect(private_bounds)]
1011pub trait CiDriver: PrivateDriver {
1012 fn on_default_branch() -> bool;
1015
1016 fn keyless_cert_identity() -> Result<String>;
1022
1023 fn oidc_provider() -> Result<String>;
1028
1029 fn generate_tags(opts: GenerateTagsOpts) -> Result<Vec<Tag>>;
1057
1058 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 fn get_repo_url() -> Result<String>;
1146
1147 fn get_registry() -> Result<String>;
1152
1153 fn default_ci_file_path() -> PathBuf;
1154}
1155
1156#[expect(private_bounds)]
1157pub trait BootDriver: PrivateDriver {
1158 fn status() -> Result<Box<dyn BootStatus>>;
1163
1164 fn switch(opts: SwitchOpts) -> Result<()>;
1169
1170 fn upgrade(opts: SwitchOpts) -> Result<()>;
1175}
1176
1177#[expect(private_bounds)]
1178pub trait BootStatus: PrivateDriver {
1179 fn transaction_in_progress(&self) -> bool;
1181
1182 fn booted_image(&self) -> Option<ImageRef<'_>>;
1184
1185 fn staged_image(&self) -> Option<ImageRef<'_>>;
1187}