1use super::*;
129use crate::chunking::{self, Chunk};
130use crate::generic_decompress::Decompressor;
131use crate::logging::system_repo_journal_print;
132use crate::refescape;
133use crate::sysroot::SysrootLock;
134use anyhow::{Context, anyhow};
135use bootc_utils::ResultExt;
136use camino::{Utf8Path, Utf8PathBuf};
137use canon_json::CanonJsonSerialize;
138use cap_std_ext::cap_std;
139use cap_std_ext::cap_std::fs::{Dir, MetadataExt};
140
141use cap_std_ext::dirext::CapStdExtDirExt;
142use containers_image_proxy::{ImageProxy, OpenedImage};
143use flate2::Compression;
144use fn_error_context::context;
145use futures_util::TryFutureExt;
146use glib::prelude::*;
147use oci_spec::image::{
148 self as oci_image, Arch, Descriptor, Digest, History, ImageConfiguration, ImageManifest,
149};
150use ocidir::oci_spec::distribution::Reference;
151use ostree::prelude::{Cast, FileEnumeratorExt, FileExt, ToVariant};
152use ostree::{gio, glib};
153use std::collections::{BTreeMap, BTreeSet, HashMap};
154use std::fmt::Write as _;
155use std::iter::FromIterator;
156use std::num::NonZeroUsize;
157use tokio::sync::mpsc::{Receiver, Sender};
158
159pub use containers_image_proxy::ImageProxyConfig;
164
165const LAYER_PREFIX: &str = "ostree/container/blob";
167const IMAGE_PREFIX: &str = "ostree/container/image";
169pub const BASE_IMAGE_PREFIX: &str = "ostree/container/baseimage";
174
175pub(crate) const META_MANIFEST_DIGEST: &str = "ostree.manifest-digest";
177const META_MANIFEST: &str = "ostree.manifest";
179const META_CONFIG: &str = "ostree.container.image-config";
181pub type MetaFilteredData = HashMap<String, HashMap<String, u32>>;
183
184const OSTREE_BASE_DEPLOYMENT_REFS: &[&str] = &["ostree/0", "ostree/1"];
186const RPMOSTREE_BASE_REFS: &[&str] = &["rpmostree/base"];
188
189fn ref_for_blob_digest(d: &str) -> Result<String> {
191 refescape::prefix_escape_for_ref(LAYER_PREFIX, d)
192}
193
194fn ref_for_layer(l: &oci_image::Descriptor) -> Result<String> {
196 ref_for_blob_digest(&l.digest().as_ref())
197}
198
199fn ref_for_image(l: &ImageReference) -> Result<String> {
201 refescape::prefix_escape_for_ref(IMAGE_PREFIX, &l.to_string())
202}
203
204#[derive(Debug)]
206pub enum ImportProgress {
207 OstreeChunkStarted(Descriptor),
209 OstreeChunkCompleted(Descriptor),
211 DerivedLayerStarted(Descriptor),
213 DerivedLayerCompleted(Descriptor),
215}
216
217impl ImportProgress {
218 pub fn is_starting(&self) -> bool {
220 match self {
221 ImportProgress::OstreeChunkStarted(_) => true,
222 ImportProgress::OstreeChunkCompleted(_) => false,
223 ImportProgress::DerivedLayerStarted(_) => true,
224 ImportProgress::DerivedLayerCompleted(_) => false,
225 }
226 }
227}
228
229#[derive(Clone, Debug)]
231pub struct LayerProgress {
232 pub layer_index: usize,
234 pub fetched: u64,
236 pub total: u64,
238}
239
240#[derive(Debug, PartialEq, Eq)]
242pub struct LayeredImageState {
243 pub base_commit: String,
245 pub merge_commit: String,
247 pub manifest_digest: Digest,
249 pub manifest: ImageManifest,
251 pub configuration: ImageConfiguration,
253 pub cached_update: Option<CachedImageUpdate>,
255 pub verify_text: Option<String>,
259 pub filtered_files: Option<MetaFilteredData>,
261}
262
263impl LayeredImageState {
264 pub fn get_commit(&self) -> &str {
268 self.merge_commit.as_str()
269 }
270
271 pub fn version(&self) -> Option<&str> {
273 super::version_for_config(&self.configuration)
274 }
275}
276
277#[derive(Debug, PartialEq, Eq)]
279pub struct CachedImageUpdate {
280 pub manifest: ImageManifest,
282 pub config: ImageConfiguration,
284 pub manifest_digest: Digest,
286}
287
288impl CachedImageUpdate {
289 pub fn version(&self) -> Option<&str> {
291 super::version_for_config(&self.config)
292 }
293}
294
295struct LayerRef {
297 ostree_ref: String,
298 commit: String,
299}
300
301#[derive(Debug)]
303pub struct ImageImporter {
304 repo: ostree::Repo,
305 root: Dir,
307 pub(crate) proxy: ImageProxy,
308 imgref: OstreeImageReference,
309 target_imgref: Option<OstreeImageReference>,
310 no_imgref: bool, disable_gc: bool, sepolicy_commit: Option<String>,
314 require_bootable: bool,
316 offline: bool,
318 ostree_v2024_3: bool,
320 diffid_to_digest: HashMap<String, String>,
322
323 layer_progress: Option<Sender<ImportProgress>>,
324 layer_byte_progress: Option<tokio::sync::watch::Sender<Option<LayerProgress>>>,
325}
326
327#[derive(Debug)]
329pub enum PrepareResult {
330 AlreadyPresent(Box<LayeredImageState>),
332 Ready(Box<PreparedImport>),
334}
335
336#[derive(Debug)]
338pub struct ManifestLayerState {
339 pub layer: oci_image::Descriptor,
341 pub ostree_ref: String,
344 pub commit: Option<String>,
347}
348
349impl ManifestLayerState {
350 pub fn layer(&self) -> &oci_image::Descriptor {
352 &self.layer
353 }
354}
355
356#[derive(Debug)]
358pub struct PreparedImport {
359 pub manifest_digest: Digest,
361 pub manifest: oci_image::ImageManifest,
363 pub config: oci_image::ImageConfiguration,
365 pub previous_state: Option<Box<LayeredImageState>>,
367 pub previous_manifest_digest: Option<Digest>,
369 pub previous_imageid: Option<String>,
371 pub ostree_layers: Vec<ManifestLayerState>,
373 pub ostree_commit_layer: Option<ManifestLayerState>,
375 pub layers: Vec<ManifestLayerState>,
377 pub verify_text: Option<String>,
379 proxy_img: OpenedImage,
381}
382
383impl PreparedImport {
384 pub fn all_layers(&self) -> impl Iterator<Item = &ManifestLayerState> {
386 self.ostree_commit_layer
387 .iter()
388 .chain(self.ostree_layers.iter())
389 .chain(self.layers.iter())
390 }
391
392 pub fn version(&self) -> Option<&str> {
394 super::version_for_config(&self.config)
395 }
396
397 pub fn deprecated_warning(&self) -> Option<&'static str> {
399 None
400 }
401
402 pub fn layers_with_history(
405 &self,
406 ) -> impl Iterator<Item = Result<(&ManifestLayerState, &History)>> {
407 let truncated = std::iter::once_with(|| Err(anyhow::anyhow!("Truncated history")));
409 let history = self
410 .config
411 .history()
412 .iter()
413 .flatten()
414 .map(Ok)
415 .chain(truncated);
416 self.all_layers()
417 .zip(history)
418 .map(|(s, h)| h.map(|h| (s, h)))
419 }
420
421 pub fn layers_to_fetch(&self) -> impl Iterator<Item = Result<(&ManifestLayerState, &str)>> {
423 self.layers_with_history().filter_map(|r| {
424 r.map(|(l, h)| {
425 l.commit.is_none().then(|| {
426 let comment = h.created_by().as_deref().unwrap_or("");
427 (l, comment)
428 })
429 })
430 .transpose()
431 })
432 }
433
434 pub(crate) fn format_layer_status(&self) -> Option<String> {
436 let (stored, to_fetch, to_fetch_size) =
437 self.all_layers()
438 .fold((0u32, 0u32, 0u64), |(stored, to_fetch, sz), v| {
439 if v.commit.is_some() {
440 (stored + 1, to_fetch, sz)
441 } else {
442 (stored, to_fetch + 1, sz + v.layer().size())
443 }
444 });
445 (to_fetch > 0).then(|| {
446 let size = crate::glib::format_size(to_fetch_size);
447 format!("layers already present: {stored}; layers needed: {to_fetch} ({size})")
448 })
449 }
450}
451
452pub(crate) fn query_layer(
454 repo: &ostree::Repo,
455 layer: oci_image::Descriptor,
456) -> Result<ManifestLayerState> {
457 let ostree_ref = ref_for_layer(&layer)?;
458 let commit = repo.resolve_rev(&ostree_ref, true)?.map(|s| s.to_string());
459 Ok(ManifestLayerState {
460 layer,
461 ostree_ref,
462 commit,
463 })
464}
465
466#[context("Reading manifest data from commit")]
467fn manifest_data_from_commitmeta(
468 commit_meta: &glib::VariantDict,
469) -> Result<(oci_image::ImageManifest, Digest)> {
470 let digest = commit_meta
471 .lookup::<String>(META_MANIFEST_DIGEST)?
472 .ok_or_else(|| anyhow!("Missing {} metadata on merge commit", META_MANIFEST_DIGEST))?;
473 let digest = Digest::from_str(&digest)?;
474 let manifest_bytes: String = commit_meta
475 .lookup::<String>(META_MANIFEST)?
476 .ok_or_else(|| anyhow!("Failed to find {} metadata key", META_MANIFEST))?;
477 let r = serde_json::from_str(&manifest_bytes)?;
478 Ok((r, digest))
479}
480
481fn image_config_from_commitmeta(commit_meta: &glib::VariantDict) -> Result<ImageConfiguration> {
482 let config = if let Some(config) = commit_meta
483 .lookup::<String>(META_CONFIG)?
484 .filter(|v| v != "null") .map(|v| serde_json::from_str(&v).map_err(anyhow::Error::msg))
486 .transpose()?
487 {
488 config
489 } else {
490 tracing::debug!("No image configuration found");
491 Default::default()
492 };
493 Ok(config)
494}
495
496pub fn manifest_digest_from_commit(commit: &glib::Variant) -> Result<Digest> {
502 let commit_meta = &commit.child_value(0);
503 let commit_meta = &glib::VariantDict::new(Some(commit_meta));
504 Ok(manifest_data_from_commitmeta(commit_meta)?.1)
505}
506
507fn layer_from_diffid<'a>(
511 manifest: &'a ImageManifest,
512 config: &ImageConfiguration,
513 diffid: &str,
514) -> Result<&'a Descriptor> {
515 let idx = config
516 .rootfs()
517 .diff_ids()
518 .iter()
519 .position(|x| x.as_str() == diffid)
520 .ok_or_else(|| anyhow!("Missing {} {}", DIFFID_LABEL, diffid))?;
521 manifest.layers().get(idx).ok_or_else(|| {
522 anyhow!(
523 "diffid position {} exceeds layer count {}",
524 idx,
525 manifest.layers().len()
526 )
527 })
528}
529
530#[context("Parsing manifest layout")]
531pub(crate) fn parse_manifest_layout<'a>(
532 manifest: &'a ImageManifest,
533 config: &ImageConfiguration,
534) -> Result<(
535 Option<&'a Descriptor>,
536 Vec<&'a Descriptor>,
537 Vec<&'a Descriptor>,
538)> {
539 let config_labels = super::labels_of(config);
540
541 let first_layer = manifest
542 .layers()
543 .first()
544 .ok_or_else(|| anyhow!("No layers in manifest"))?;
545 let Some(target_diffid) = config_labels.and_then(|labels| labels.get(DIFFID_LABEL)) else {
546 return Ok((None, Vec::new(), manifest.layers().iter().collect()));
547 };
548
549 let target_layer = layer_from_diffid(manifest, config, target_diffid.as_str())?;
550 let mut chunk_layers = Vec::new();
551 let mut derived_layers = Vec::new();
552 let mut after_target = false;
553 let ostree_layer = first_layer;
555 for layer in manifest.layers() {
556 if layer == target_layer {
557 if after_target {
558 anyhow::bail!("Multiple entries for {}", layer.digest());
559 }
560 after_target = true;
561 if layer != ostree_layer {
562 chunk_layers.push(layer);
563 }
564 } else if !after_target {
565 if layer != ostree_layer {
566 chunk_layers.push(layer);
567 }
568 } else {
569 derived_layers.push(layer);
570 }
571 }
572
573 Ok((Some(ostree_layer), chunk_layers, derived_layers))
574}
575
576#[context("Parsing manifest layout")]
578pub(crate) fn parse_ostree_manifest_layout<'a>(
579 manifest: &'a ImageManifest,
580 config: &ImageConfiguration,
581) -> Result<(&'a Descriptor, Vec<&'a Descriptor>, Vec<&'a Descriptor>)> {
582 let (ostree_layer, component_layers, derived_layers) = parse_manifest_layout(manifest, config)?;
583 let ostree_layer = ostree_layer.ok_or_else(|| {
584 anyhow!("No {DIFFID_LABEL} label found, not an ostree encapsulated container")
585 })?;
586 Ok((ostree_layer, component_layers, derived_layers))
587}
588
589fn timestamp_of_manifest_or_config(
591 manifest: &ImageManifest,
592 config: &ImageConfiguration,
593) -> Option<u64> {
594 let timestamp = manifest
597 .annotations()
598 .as_ref()
599 .and_then(|a| a.get(oci_image::ANNOTATION_CREATED))
600 .or_else(|| config.created().as_ref());
601 timestamp
603 .map(|t| {
604 chrono::DateTime::parse_from_rfc3339(t)
605 .context("Failed to parse manifest timestamp")
606 .map(|t| t.timestamp() as u64)
607 })
608 .transpose()
609 .log_err_default()
610}
611
612fn cleanup_root(root: &Dir) -> Result<()> {
615 const RUNTIME_INJECTED: &[&str] = &["usr/etc/hostname", "usr/etc/resolv.conf"];
616 for ent in RUNTIME_INJECTED {
617 if let Some(meta) = root.symlink_metadata_optional(ent)? {
618 if meta.is_file() && meta.size() == 0 {
619 tracing::debug!("Removing {ent}");
620 root.remove_file(ent)?;
621 }
622 }
623 }
624 Ok(())
625}
626
627impl ImageImporter {
628 const CACHED_KEY_MANIFEST_DIGEST: &'static str = "ostree-ext.cached.manifest-digest";
630 const CACHED_KEY_MANIFEST: &'static str = "ostree-ext.cached.manifest";
631 const CACHED_KEY_CONFIG: &'static str = "ostree-ext.cached.config";
632
633 #[context("Creating importer")]
635 pub async fn new(
636 repo: &ostree::Repo,
637 imgref: &OstreeImageReference,
638 mut config: ImageProxyConfig,
639 ) -> Result<Self> {
640 apply_container_proxy_opts_for_transport(&mut config, imgref.imgref.transport)?;
641 let proxy = ImageProxy::new_with_config(config).await?;
642
643 system_repo_journal_print(
644 repo,
645 libsystemd::logging::Priority::Info,
646 &format!("Fetching {imgref}"),
647 );
648
649 let repo = repo.clone();
650
651 let diffid_to_digest = Self::build_diffid_to_digest_map(&repo)?;
652
653 let root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
654
655 Ok(ImageImporter {
656 repo,
657 root,
658 proxy,
659 target_imgref: None,
660 no_imgref: false,
661 ostree_v2024_3: ostree::check_version(2024, 3),
662 disable_gc: false,
663 sepolicy_commit: None,
664 require_bootable: false,
665 offline: false,
666 imgref: imgref.clone(),
667 diffid_to_digest,
668 layer_progress: None,
669 layer_byte_progress: None,
670 })
671 }
672
673 pub fn set_target(&mut self, target: &OstreeImageReference) {
675 self.target_imgref = Some(target.clone())
676 }
677
678 pub fn set_no_imgref(&mut self) {
682 self.no_imgref = true;
683 }
684
685 pub fn set_offline(&mut self) {
687 self.offline = true;
688 }
689
690 pub fn require_bootable(&mut self) {
692 self.require_bootable = true;
693 }
694
695 pub fn set_ostree_version(&mut self, year: u32, v: u32) {
697 self.ostree_v2024_3 = (year > 2024) || (year == 2024 && v >= 3)
698 }
699
700 pub fn disable_gc(&mut self) {
702 self.disable_gc = true;
703 }
704
705 pub fn set_sepolicy_commit(&mut self, commit: String) {
709 self.sepolicy_commit = Some(commit);
710 }
711
712 #[context("Preparing import")]
717 pub async fn prepare(&mut self) -> Result<PrepareResult> {
718 self.prepare_internal(false).await
719 }
720
721 pub fn request_progress(&mut self) -> Receiver<ImportProgress> {
723 assert!(self.layer_progress.is_none());
724 let (s, r) = tokio::sync::mpsc::channel(2);
725 self.layer_progress = Some(s);
726 r
727 }
728
729 pub fn request_layer_progress(
731 &mut self,
732 ) -> tokio::sync::watch::Receiver<Option<LayerProgress>> {
733 assert!(self.layer_byte_progress.is_none());
734 let (s, r) = tokio::sync::watch::channel(None);
735 self.layer_byte_progress = Some(s);
736 r
737 }
738
739 #[context("Writing cached pending manifest")]
742 pub(crate) async fn cache_pending(
743 &self,
744 commit: &str,
745 manifest_digest: &Digest,
746 manifest: &ImageManifest,
747 config: &ImageConfiguration,
748 ) -> Result<()> {
749 let commitmeta = glib::VariantDict::new(None);
750 commitmeta.insert(
751 Self::CACHED_KEY_MANIFEST_DIGEST,
752 manifest_digest.to_string(),
753 );
754 let cached_manifest = manifest
755 .to_canon_json_string()
756 .context("Serializing manifest")?;
757 commitmeta.insert(Self::CACHED_KEY_MANIFEST, cached_manifest);
758 let cached_config = config
759 .to_canon_json_string()
760 .context("Serializing config")?;
761 commitmeta.insert(Self::CACHED_KEY_CONFIG, cached_config);
762 let commitmeta = commitmeta.to_variant();
763 let commit = commit.to_string();
765 let repo = self.repo.clone();
766 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
767 repo.write_commit_detached_metadata(&commit, Some(&commitmeta), Some(cancellable))
768 .map_err(anyhow::Error::msg)
769 })
770 .await
771 }
772
773 fn build_diffid_to_digest_map(repo: &ostree::Repo) -> Result<HashMap<String, String>> {
776 let mut map = HashMap::new();
777 let all_images = list_images(repo)?;
778
779 for imgref_str in all_images {
780 let imgref = match ImageReference::try_from(imgref_str.as_str()) {
781 Ok(r) => r,
782 Err(e) => {
783 tracing::warn!("Failed to parse image reference {}: {}", imgref_str, e);
784 continue;
785 }
786 };
787
788 let state = match query_image(repo, &imgref)? {
789 Some(s) => s,
790 None => continue,
791 };
792
793 for (layer_desc, diff_id) in state
795 .manifest
796 .layers()
797 .iter()
798 .zip(state.configuration.rootfs().diff_ids())
799 {
800 let diff_id_str = diff_id.to_string();
801 map.entry(diff_id_str)
803 .or_insert_with(|| layer_desc.digest().to_string());
804 }
805 }
806
807 Ok(map)
808 }
809
810 fn find_digest_by_diffid(
811 &self,
812 manifest: &oci_image::ImageManifest,
813 config: &oci_image::ImageConfiguration,
814 layer: &oci_image::Descriptor,
815 ) -> Option<&String> {
816 let idx = manifest
817 .layers()
818 .iter()
819 .position(|l| l.digest() == layer.digest())?;
820 let layer_diffid = config.rootfs().diff_ids().get(idx)?;
821 self.diffid_to_digest.get(layer_diffid.as_str())
822 }
823
824 fn resolve_commit_by_diffid(
826 &self,
827 manifest: &oci_image::ImageManifest,
828 config: &oci_image::ImageConfiguration,
829 layer: &oci_image::Descriptor,
830 ) -> Result<Option<String>> {
831 let Some(existing_digest) = self.find_digest_by_diffid(manifest, config, layer) else {
832 return Ok(None);
833 };
834 let existing_ref = ref_for_blob_digest(existing_digest)?;
835 Ok(self
836 .repo
837 .resolve_rev(&existing_ref, true)?
838 .map(|s| s.to_string()))
839 }
840
841 fn query_layer(
844 &self,
845 manifest: &oci_image::ImageManifest,
846 config: &oci_image::ImageConfiguration,
847 layer: &oci_image::Descriptor,
848 ) -> Result<ManifestLayerState> {
849 let ostree_ref = ref_for_layer(layer)?;
850 let commit = self
851 .repo
852 .resolve_rev(&ostree_ref, true)?
853 .map(|s| s.to_string());
854 let commit = match commit {
857 Some(c) => Some(c),
858 None => self.resolve_commit_by_diffid(manifest, config, layer)?,
859 };
860
861 Ok(ManifestLayerState {
862 layer: layer.clone(),
863 ostree_ref,
864 commit,
865 })
866 }
867
868 fn ensure_ref_for_layer(repo: &ostree::Repo, ostree_ref: &str, commit: &str) -> Result<()> {
870 let ref_exists = repo.resolve_rev(ostree_ref, true)?.is_some();
871 if !ref_exists {
872 tracing::debug!("Creating ref {} for reused commit {}", ostree_ref, commit);
873 repo.set_ref_immediate(None, ostree_ref, Some(commit), gio::Cancellable::NONE)?;
874 }
875 Ok(())
876 }
877
878 fn create_prepared_import(
881 &mut self,
882 manifest_digest: Digest,
883 manifest: ImageManifest,
884 config: ImageConfiguration,
885 previous_state: Option<Box<LayeredImageState>>,
886 previous_imageid: Option<String>,
887 proxy_img: OpenedImage,
888 ) -> Result<Box<PreparedImport>> {
889 let config_labels = super::labels_of(&config);
890 if self.require_bootable {
891 let bootable_key = ostree::METADATA_KEY_BOOTABLE;
892 let bootable = config_labels.is_some_and(|l| {
893 l.contains_key(bootable_key.as_str()) || l.contains_key(BOOTC_LABEL)
894 });
895 if !bootable {
896 anyhow::bail!("Target image does not have {bootable_key} label");
897 }
898 let container_arch = config.architecture();
899 let target_arch = &Arch::default();
900 if container_arch != target_arch {
901 anyhow::bail!("Image has architecture {container_arch}; expected {target_arch}");
902 }
903 }
904
905 let (commit_layer, component_layers, remaining_layers) =
906 parse_manifest_layout(&manifest, &config)?;
907
908 let commit_layer = commit_layer
909 .map(|layer| self.query_layer(&manifest, &config, layer))
910 .transpose()?;
911
912 let component_layers = component_layers
913 .into_iter()
914 .map(|l| self.query_layer(&manifest, &config, l))
915 .collect::<Result<Vec<_>>>()?;
916
917 let remaining_layers = remaining_layers
918 .into_iter()
919 .map(|l| self.query_layer(&manifest, &config, l))
920 .collect::<Result<Vec<_>>>()?;
921
922 let previous_manifest_digest = previous_state.as_ref().map(|s| s.manifest_digest.clone());
923 let imp = PreparedImport {
924 manifest_digest,
925 manifest,
926 config,
927 previous_state,
928 previous_manifest_digest,
929 previous_imageid,
930 ostree_layers: component_layers,
931 ostree_commit_layer: commit_layer,
932 layers: remaining_layers,
933 verify_text: None,
934 proxy_img,
935 };
936 Ok(Box::new(imp))
937 }
938
939 #[context("Fetching manifest")]
941 pub(crate) async fn prepare_internal(&mut self, verify_layers: bool) -> Result<PrepareResult> {
942 match &self.imgref.sigverify {
943 SignatureSource::ContainerPolicy
944 if skopeo::container_policy_is_default_insecure(&self.root)? =>
945 {
946 return Err(anyhow!(
947 "containers-policy.json specifies a default of `insecureAcceptAnything`; refusing usage"
948 ));
949 }
950 SignatureSource::OstreeRemote(_) if verify_layers => {
951 return Err(anyhow!(
952 "Cannot currently verify layered containers via ostree remote"
953 ));
954 }
955 _ => {}
956 }
957
958 let previous_state = try_query_image(&self.repo, &self.imgref.imgref)?;
960
961 let target_reference = self.imgref.imgref.name.parse::<Reference>().ok();
963 let previous_state = if let Some(target_digest) = target_reference
964 .as_ref()
965 .and_then(|v| v.digest())
966 .map(Digest::from_str)
967 .transpose()?
968 {
969 if let Some(previous_state) = previous_state {
970 if previous_state.manifest_digest == target_digest {
972 tracing::debug!("Digest-based pullspec {:?} already present", self.imgref);
973 return Ok(PrepareResult::AlreadyPresent(previous_state));
974 }
975 Some(previous_state)
976 } else {
977 None
978 }
979 } else {
980 previous_state
981 };
982
983 if self.offline {
984 anyhow::bail!("Manifest fetch required in offline mode");
985 }
986
987 let proxy_img = self
988 .proxy
989 .open_image(&self.imgref.imgref.to_string())
990 .await?;
991
992 let (manifest_digest, manifest) = self.proxy.fetch_manifest(&proxy_img).await?;
993 let manifest_digest = Digest::from_str(&manifest_digest)?;
994 let new_imageid = manifest.config().digest();
995
996 let (previous_state, previous_imageid) = if let Some(previous_state) = previous_state {
999 if previous_state.manifest_digest == manifest_digest {
1001 return Ok(PrepareResult::AlreadyPresent(previous_state));
1002 }
1003 let previous_imageid = previous_state.manifest.config().digest();
1005 if previous_imageid == new_imageid {
1006 return Ok(PrepareResult::AlreadyPresent(previous_state));
1007 }
1008 let previous_imageid = previous_imageid.to_string();
1009 (Some(previous_state), Some(previous_imageid))
1010 } else {
1011 (None, None)
1012 };
1013
1014 let config = self.proxy.fetch_config(&proxy_img).await?;
1015
1016 if let Some(previous_state) = previous_state.as_ref() {
1019 self.cache_pending(
1020 previous_state.merge_commit.as_str(),
1021 &manifest_digest,
1022 &manifest,
1023 &config,
1024 )
1025 .await?;
1026 }
1027
1028 let imp = self.create_prepared_import(
1029 manifest_digest,
1030 manifest,
1031 config,
1032 previous_state,
1033 previous_imageid,
1034 proxy_img,
1035 )?;
1036 Ok(PrepareResult::Ready(imp))
1037 }
1038
1039 #[context("Unencapsulating base")]
1041 pub(crate) async fn unencapsulate_base(
1042 &self,
1043 import: &mut store::PreparedImport,
1044 require_ostree: bool,
1045 write_refs: bool,
1046 ) -> Result<()> {
1047 tracing::debug!("Fetching base");
1048 if matches!(self.imgref.sigverify, SignatureSource::ContainerPolicy)
1049 && skopeo::container_policy_is_default_insecure(&self.root)?
1050 {
1051 return Err(anyhow!(
1052 "containers-policy.json specifies a default of `insecureAcceptAnything`; refusing usage"
1053 ));
1054 }
1055 let remote = match &self.imgref.sigverify {
1056 SignatureSource::OstreeRemote(remote) => Some(remote.clone()),
1057 SignatureSource::ContainerPolicy | SignatureSource::ContainerPolicyAllowInsecure => {
1058 None
1059 }
1060 };
1061 let Some(commit_layer) = import.ostree_commit_layer.as_mut() else {
1062 if require_ostree {
1063 anyhow::bail!(
1064 "No {DIFFID_LABEL} label found, not an ostree encapsulated container"
1065 );
1066 }
1067 return Ok(());
1068 };
1069 let des_layers = self.proxy.get_layer_info(&import.proxy_img).await?;
1070 for layer in import.ostree_layers.iter_mut() {
1071 if let Some(commit) = layer.commit.as_ref() {
1072 if write_refs {
1073 Self::ensure_ref_for_layer(&self.repo, &layer.ostree_ref, commit)?;
1074 }
1075 continue;
1076 }
1077 if let Some(p) = self.layer_progress.as_ref() {
1078 p.send(ImportProgress::OstreeChunkStarted(layer.layer.clone()))
1079 .await?;
1080 }
1081 let (blob, driver, media_type) = fetch_layer(
1082 &self.proxy,
1083 &import.proxy_img,
1084 &import.manifest,
1085 &layer.layer,
1086 self.layer_byte_progress.as_ref(),
1087 des_layers.as_ref(),
1088 self.imgref.imgref.transport,
1089 )
1090 .await?;
1091 let repo = self.repo.clone();
1092 let target_ref = layer.ostree_ref.clone();
1093 let import_task =
1094 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
1095 let txn = repo.auto_transaction(Some(cancellable))?;
1096 let mut importer = crate::tar::Importer::new_for_object_set(&repo);
1097 let blob = tokio_util::io::SyncIoBridge::new(blob);
1098 let mut blob = Decompressor::new(&media_type, blob)?;
1099 let mut archive = tar::Archive::new(&mut blob);
1100 importer.import_objects(&mut archive, Some(cancellable))?;
1101 let commit = if write_refs {
1102 let commit = importer.finish_import_object_set()?;
1103 repo.transaction_set_ref(None, &target_ref, Some(commit.as_str()));
1104 tracing::debug!("Wrote {} => {}", target_ref, commit);
1105 Some(commit)
1106 } else {
1107 None
1108 };
1109 txn.commit(Some(cancellable))?;
1110 blob.finish()?;
1111 Ok::<_, anyhow::Error>(commit)
1112 })
1113 .map_err(|e| e.context(format!("Layer {}", layer.layer.digest())));
1114 let commit = super::unencapsulate::join_fetch(import_task, driver).await?;
1115 layer.commit = commit;
1116 if let Some(p) = self.layer_progress.as_ref() {
1117 p.send(ImportProgress::OstreeChunkCompleted(layer.layer.clone()))
1118 .await?;
1119 }
1120 }
1121 if let Some(commit) = commit_layer.commit.as_ref() {
1122 if write_refs {
1123 Self::ensure_ref_for_layer(&self.repo, &commit_layer.ostree_ref, commit)?;
1124 }
1125 } else {
1126 if let Some(p) = self.layer_progress.as_ref() {
1127 p.send(ImportProgress::OstreeChunkStarted(
1128 commit_layer.layer.clone(),
1129 ))
1130 .await?;
1131 }
1132 let (blob, driver, media_type) = fetch_layer(
1133 &self.proxy,
1134 &import.proxy_img,
1135 &import.manifest,
1136 &commit_layer.layer,
1137 self.layer_byte_progress.as_ref(),
1138 des_layers.as_ref(),
1139 self.imgref.imgref.transport,
1140 )
1141 .await?;
1142 let repo = self.repo.clone();
1143 let target_ref = commit_layer.ostree_ref.clone();
1144 let import_task =
1145 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
1146 let txn = repo.auto_transaction(Some(cancellable))?;
1147 let mut importer = crate::tar::Importer::new_for_commit(&repo, remote);
1148 let blob = tokio_util::io::SyncIoBridge::new(blob);
1149 let mut blob = Decompressor::new(&media_type, blob)?;
1150 let mut archive = tar::Archive::new(&mut blob);
1151 importer.import_commit(&mut archive, Some(cancellable))?;
1152 let (commit, verify_text) = importer.finish_import_commit();
1153 if write_refs {
1154 repo.transaction_set_ref(None, &target_ref, Some(commit.as_str()));
1155 tracing::debug!("Wrote {} => {}", target_ref, commit);
1156 }
1157 repo.mark_commit_partial(&commit, false)?;
1158 txn.commit(Some(cancellable))?;
1159 blob.finish()?;
1160 Ok::<_, anyhow::Error>((commit, verify_text))
1161 });
1162 let (commit, verify_text) =
1163 super::unencapsulate::join_fetch(import_task, driver).await?;
1164 commit_layer.commit = Some(commit);
1165 import.verify_text = verify_text;
1166 if let Some(p) = self.layer_progress.as_ref() {
1167 p.send(ImportProgress::OstreeChunkCompleted(
1168 commit_layer.layer.clone(),
1169 ))
1170 .await?;
1171 }
1172 }
1173 Ok(())
1174 }
1175
1176 pub async fn unencapsulate(mut self) -> Result<Import> {
1181 let mut prep = match self.prepare_internal(false).await? {
1182 PrepareResult::AlreadyPresent(_) => {
1183 panic!("Should not have image present for unencapsulation")
1184 }
1185 PrepareResult::Ready(r) => r,
1186 };
1187 if !prep.layers.is_empty() {
1188 anyhow::bail!("Image has {} non-ostree layers", prep.layers.len());
1189 }
1190 let deprecated_warning = prep.deprecated_warning().map(ToOwned::to_owned);
1191 self.unencapsulate_base(&mut prep, true, false).await?;
1192 self.proxy.close_image(&prep.proxy_img).await?;
1195 let ostree_commit = prep.ostree_commit_layer.unwrap().commit.unwrap();
1197 let image_digest = prep.manifest_digest;
1198 Ok(Import {
1199 ostree_commit,
1200 image_digest,
1201 deprecated_warning,
1202 })
1203 }
1204
1205 fn write_merge_commit_impl(
1208 repo: &ostree::Repo,
1209 base_commit: Option<&str>,
1210 layer_commits: &[LayerRef],
1211 have_derived_layers: bool,
1212 metadata: glib::Variant,
1213 timestamp: u64,
1214 ostree_ref: &str,
1215 no_imgref: bool,
1216 disable_gc: bool,
1217 cancellable: Option<&gio::Cancellable>,
1218 ) -> Result<Box<LayeredImageState>> {
1219 use rustix::fd::AsRawFd;
1220
1221 let txn = repo.auto_transaction(cancellable)?;
1222
1223 let devino = ostree::RepoDevInoCache::new();
1224 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
1225 let repo_tmp = repodir.open_dir("tmp")?;
1226 let td = cap_std_ext::cap_tempfile::TempDir::new_in(&repo_tmp)?;
1227
1228 let rootpath = "root";
1229 let checkout_mode = if repo.mode() == ostree::RepoMode::Bare {
1230 ostree::RepoCheckoutMode::None
1231 } else {
1232 ostree::RepoCheckoutMode::User
1233 };
1234 let mut checkout_opts = ostree::RepoCheckoutAtOptions {
1235 mode: checkout_mode,
1236 overwrite_mode: ostree::RepoCheckoutOverwriteMode::UnionFiles,
1237 devino_to_csum_cache: Some(devino.clone()),
1238 no_copy_fallback: true,
1239 force_copy_zerosized: true,
1240 process_whiteouts: false,
1241 ..Default::default()
1242 };
1243 if let Some(base) = base_commit.as_ref() {
1244 repo.checkout_at(
1245 Some(&checkout_opts),
1246 (*td).as_raw_fd(),
1247 rootpath,
1248 &base,
1249 cancellable,
1250 )
1251 .context("Checking out base commit")?;
1252 }
1253
1254 checkout_opts.process_whiteouts = true;
1256 for lc in layer_commits {
1257 let commit = &lc.commit;
1258 tracing::debug!("Unpacking {commit}");
1259 repo.checkout_at(
1260 Some(&checkout_opts),
1261 (*td).as_raw_fd(),
1262 rootpath,
1263 commit,
1264 cancellable,
1265 )
1266 .with_context(|| format!("Checking out layer {commit}"))?;
1267 }
1268
1269 let root_dir = td.open_dir(rootpath)?;
1270
1271 let modifier =
1272 ostree::RepoCommitModifier::new(ostree::RepoCommitModifierFlags::empty(), None);
1273 modifier.set_devino_cache(&devino);
1274 let should_relabel;
1282 if have_derived_layers {
1283 let sepolicy = ostree::SePolicy::new_at(root_dir.as_raw_fd(), cancellable)?;
1284 should_relabel = sepolicy.name().map_or(false, |s| !s.is_empty());
1285 if should_relabel {
1286 tracing::debug!("labeling from merged tree");
1287 modifier.set_sepolicy(Some(&sepolicy));
1288 }
1289 } else if let Some(base) = base_commit.as_ref() {
1290 tracing::debug!("labeling from base tree");
1291 should_relabel = false;
1292 modifier.set_sepolicy_from_commit(repo, &base, cancellable)?;
1294 } else {
1295 panic!("Unexpected state: no derived layers and no base")
1296 }
1297
1298 cleanup_root(&root_dir)?;
1299
1300 let mt = ostree::MutableTree::new();
1301 repo.write_dfd_to_mtree(
1302 (*td).as_raw_fd(),
1303 rootpath,
1304 &mt,
1305 Some(&modifier),
1306 cancellable,
1307 )
1308 .context("Writing merged filesystem to mtree")?;
1309
1310 let merged_root = repo
1311 .write_mtree(&mt, cancellable)
1312 .context("Writing mtree")?;
1313 let merged_root = merged_root.downcast::<ostree::RepoFile>().unwrap();
1314 let parent = base_commit.as_deref();
1317 let merged_commit = repo
1318 .write_commit_with_time(
1319 parent,
1320 None,
1321 None,
1322 Some(&metadata),
1323 &merged_root,
1324 timestamp,
1325 cancellable,
1326 )
1327 .context("Writing commit")?;
1328 if !no_imgref {
1329 repo.transaction_set_ref(None, ostree_ref, Some(merged_commit.as_str()));
1330 }
1331
1332 let n_relabeled_layers = if should_relabel {
1338 let n =
1339 Self::relabel_layers(repo, layer_commits, &modifier, checkout_mode, cancellable)?;
1340 tracing::debug!("relabeled {n} layer commits");
1341 n
1342 } else {
1343 0
1344 };
1345
1346 txn.commit(cancellable)?;
1347
1348 if !disable_gc {
1349 let n: u32 = gc_image_layers_impl(repo, cancellable)?;
1350 tracing::debug!("pruned {n} layers");
1351 if n_relabeled_layers > 0 {
1353 let (_, n_pruned, _) =
1354 repo.prune(ostree::RepoPruneFlags::REFS_ONLY, 0, cancellable)?;
1355 tracing::debug!("pruned {n_pruned} orphaned objects after relabeling");
1356 }
1357 }
1358
1359 let state = query_image_commit(repo, &merged_commit)?;
1362 Ok(state)
1363 }
1364
1365 fn relabel_layers(
1371 repo: &ostree::Repo,
1372 layer_commits: &[LayerRef],
1373 modifier: &ostree::RepoCommitModifier,
1374 checkout_mode: ostree::RepoCheckoutMode,
1375 cancellable: Option<&gio::Cancellable>,
1376 ) -> Result<u32> {
1377 use rustix::fd::AsRawFd;
1378
1379 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
1380 let repo_tmp = repodir.open_dir("tmp")?;
1381 let rootpath = "root";
1382 let mut n_relabeled = 0u32;
1383 let checkout_opts = ostree::RepoCheckoutAtOptions {
1384 mode: checkout_mode,
1385 no_copy_fallback: true,
1386 force_copy_zerosized: true,
1387 ..Default::default()
1388 };
1389
1390 for lc in layer_commits {
1391 let (layer_ref, old_commit) = (&lc.ostree_ref, &lc.commit);
1392 let td = cap_std_ext::cap_tempfile::TempDir::new_in(&repo_tmp)?;
1393 repo.checkout_at(
1394 Some(&checkout_opts),
1395 (*td).as_raw_fd(),
1396 rootpath,
1397 old_commit,
1398 cancellable,
1399 )
1400 .with_context(|| format!("Checking out layer {old_commit} for relabeling"))?;
1401
1402 let mt = ostree::MutableTree::new();
1403 repo.write_dfd_to_mtree(
1404 (*td).as_raw_fd(),
1405 rootpath,
1406 &mt,
1407 Some(modifier),
1408 cancellable,
1409 )
1410 .with_context(|| format!("Writing relabeled layer {old_commit} to mtree"))?;
1411
1412 let root = repo
1413 .write_mtree(&mt, cancellable)
1414 .context("Writing mtree")?;
1415 let root = root.downcast::<ostree::RepoFile>().unwrap();
1416
1417 let (commit_v, _) = repo.load_commit(old_commit)?;
1419 let old_metadata = commit_v.child_value(0);
1420 let old_timestamp = ostree::commit_get_timestamp(&commit_v);
1421
1422 let new_commit = repo
1423 .write_commit_with_time(
1424 None,
1425 None,
1426 None,
1427 Some(&old_metadata),
1428 &root,
1429 old_timestamp,
1430 cancellable,
1431 )
1432 .with_context(|| format!("Writing relabeled commit for layer {layer_ref}"))?;
1433
1434 if new_commit != *old_commit {
1435 repo.transaction_set_ref(None, layer_ref, Some(new_commit.as_str()));
1436 n_relabeled += 1;
1437 tracing::debug!("Relabeled layer {layer_ref}: {old_commit} -> {new_commit}");
1438 }
1439 }
1440
1441 Ok(n_relabeled)
1442 }
1443
1444 #[context("Importing")]
1448 pub async fn import(
1449 mut self,
1450 mut import: Box<PreparedImport>,
1451 ) -> Result<Box<LayeredImageState>> {
1452 if let Some(status) = import.format_layer_status() {
1453 system_repo_journal_print(&self.repo, libsystemd::logging::Priority::Info, &status);
1454 }
1455 self.unencapsulate_base(&mut import, false, true).await?;
1458 let des_layers = self.proxy.get_layer_info(&import.proxy_img).await?;
1459 let proxy = self.proxy;
1460 let target_imgref = self.target_imgref.as_ref().unwrap_or(&self.imgref);
1461 let base_commit = import
1462 .ostree_commit_layer
1463 .as_ref()
1464 .map(|c| c.commit.clone().unwrap());
1465
1466 let root_is_transient = if let Some(base) = base_commit.as_ref() {
1467 let rootf = self.repo.read_commit(&base, gio::Cancellable::NONE)?.0;
1468 let rootf = rootf.downcast_ref::<ostree::RepoFile>().unwrap();
1469 crate::ostree_prepareroot::overlayfs_root_enabled(rootf)?
1470 } else {
1471 true
1473 };
1474 tracing::debug!("Base rootfs is transient: {root_is_transient}");
1475
1476 let ostree_ref = ref_for_image(&target_imgref.imgref)?;
1477
1478 let mut layer_commits: Vec<LayerRef> = Vec::new();
1479 let mut layer_filtered_content: Option<MetaFilteredData> = None;
1480 let have_derived_layers = !import.layers.is_empty();
1481 tracing::debug!("Processing layers: {}", import.layers.len());
1482 for layer in import.layers {
1483 if let Some(c) = layer.commit {
1484 tracing::debug!("Reusing fetched commit {}", c);
1485 Self::ensure_ref_for_layer(&self.repo, &layer.ostree_ref, &c)?;
1486
1487 layer_commits.push(LayerRef {
1488 ostree_ref: layer.ostree_ref.clone(),
1489 commit: c.to_string(),
1490 });
1491 } else {
1492 if let Some(p) = self.layer_progress.as_ref() {
1493 p.send(ImportProgress::DerivedLayerStarted(layer.layer.clone()))
1494 .await?;
1495 }
1496 let (blob, driver, media_type) = super::unencapsulate::fetch_layer(
1497 &proxy,
1498 &import.proxy_img,
1499 &import.manifest,
1500 &layer.layer,
1501 self.layer_byte_progress.as_ref(),
1502 des_layers.as_ref(),
1503 self.imgref.imgref.transport,
1504 )
1505 .await?;
1506 let opts = crate::tar::WriteTarOptions {
1510 base: base_commit.clone().or(self.sepolicy_commit.clone()),
1511 selinux: true,
1512 allow_nonusr: root_is_transient,
1513 retain_var: self.ostree_v2024_3,
1514 };
1515 let r = crate::tar::write_tar(
1516 &self.repo,
1517 blob,
1518 media_type,
1519 layer.ostree_ref.as_str(),
1520 Some(opts),
1521 );
1522 let r = super::unencapsulate::join_fetch(r, driver)
1523 .await
1524 .with_context(|| format!("Parsing layer blob {}", layer.layer.digest()))?;
1525 tracing::debug!("Imported layer: {}", r.commit.as_str());
1526 layer_commits.push(LayerRef {
1527 ostree_ref: layer.ostree_ref.clone(),
1528 commit: r.commit,
1529 });
1530 let filtered_owned = HashMap::from_iter(r.filtered.clone());
1531 if let Some((filtered, n_rest)) = bootc_utils::collect_until(
1532 r.filtered.iter(),
1533 const { NonZeroUsize::new(5).unwrap() },
1534 ) {
1535 let mut msg = String::new();
1536 for (path, n) in filtered {
1537 write!(msg, "{path}: {n} ").unwrap();
1538 }
1539 if n_rest > 0 {
1540 write!(msg, "...and {n_rest} more").unwrap();
1541 }
1542 tracing::debug!("Found filtered toplevels: {msg}");
1543 layer_filtered_content
1544 .get_or_insert_default()
1545 .insert(layer.layer.digest().to_string(), filtered_owned);
1546 } else {
1547 tracing::debug!("No filtered content");
1548 }
1549 if let Some(p) = self.layer_progress.as_ref() {
1550 p.send(ImportProgress::DerivedLayerCompleted(layer.layer.clone()))
1551 .await?;
1552 }
1553 }
1554 }
1555
1556 proxy.close_image(&import.proxy_img).await?;
1559
1560 proxy.finalize().await?;
1562 tracing::debug!("finalized proxy");
1563
1564 let _ = self.layer_byte_progress.take();
1566 let _ = self.layer_progress.take();
1567
1568 let mut metadata = BTreeMap::new();
1569 metadata.insert(
1570 META_MANIFEST_DIGEST,
1571 import.manifest_digest.to_string().to_variant(),
1572 );
1573 metadata.insert(
1574 META_MANIFEST,
1575 import.manifest.to_canon_json_string()?.to_variant(),
1576 );
1577 metadata.insert(
1578 META_CONFIG,
1579 import.config.to_canon_json_string()?.to_variant(),
1580 );
1581 metadata.insert(
1582 "ostree.importer.version",
1583 env!("CARGO_PKG_VERSION").to_variant(),
1584 );
1585 let metadata = metadata.to_variant();
1586
1587 let timestamp = timestamp_of_manifest_or_config(&import.manifest, &import.config)
1588 .unwrap_or_else(|| chrono::offset::Utc::now().timestamp() as u64);
1589 let repo = self.repo;
1591 let mut state = crate::tokio_util::spawn_blocking_cancellable_flatten(
1592 move |cancellable| -> Result<Box<LayeredImageState>> {
1593 Self::write_merge_commit_impl(
1594 &repo,
1595 base_commit.as_deref(),
1596 &layer_commits,
1597 have_derived_layers,
1598 metadata,
1599 timestamp,
1600 &ostree_ref,
1601 self.no_imgref,
1602 self.disable_gc,
1603 Some(cancellable),
1604 )
1605 },
1606 )
1607 .await?;
1608 state.verify_text = import.verify_text;
1610 state.filtered_files = layer_filtered_content;
1611 Ok(state)
1612 }
1613}
1614
1615pub fn list_images(repo: &ostree::Repo) -> Result<Vec<String>> {
1617 let cancellable = gio::Cancellable::NONE;
1618 let refs = repo.list_refs_ext(
1619 Some(IMAGE_PREFIX),
1620 ostree::RepoListRefsExtFlags::empty(),
1621 cancellable,
1622 )?;
1623 refs.keys()
1624 .map(|imgname| refescape::unprefix_unescape_ref(IMAGE_PREFIX, imgname))
1625 .collect()
1626}
1627
1628fn try_query_image(
1631 repo: &ostree::Repo,
1632 imgref: &ImageReference,
1633) -> Result<Option<Box<LayeredImageState>>> {
1634 let ostree_ref = &ref_for_image(imgref)?;
1635 if let Some(merge_rev) = repo.resolve_rev(ostree_ref, true)? {
1636 match query_image_commit(repo, merge_rev.as_str()) {
1637 Ok(r) => Ok(Some(r)),
1638 Err(e) => {
1639 eprintln!("error: failed to query image commit: {e}");
1640 Ok(None)
1641 }
1642 }
1643 } else {
1644 Ok(None)
1645 }
1646}
1647
1648#[context("Querying image {imgref}")]
1650pub fn query_image(
1651 repo: &ostree::Repo,
1652 imgref: &ImageReference,
1653) -> Result<Option<Box<LayeredImageState>>> {
1654 let ostree_ref = &ref_for_image(imgref)?;
1655 let merge_rev = repo.resolve_rev(ostree_ref, true)?;
1656 merge_rev
1657 .map(|r| query_image_commit(repo, r.as_str()))
1658 .transpose()
1659}
1660
1661fn parse_cached_update(meta: &glib::VariantDict) -> Result<Option<CachedImageUpdate>> {
1663 let manifest_digest =
1665 if let Some(d) = meta.lookup::<String>(ImageImporter::CACHED_KEY_MANIFEST_DIGEST)? {
1666 d
1667 } else {
1668 return Ok(None);
1671 };
1672 let manifest_digest = Digest::from_str(&manifest_digest)?;
1673 let manifest = meta.lookup_value(ImageImporter::CACHED_KEY_MANIFEST, None);
1676 let manifest: oci_image::ImageManifest = manifest
1677 .as_ref()
1678 .and_then(|v| v.str())
1679 .map(serde_json::from_str)
1680 .transpose()?
1681 .ok_or_else(|| {
1682 anyhow!(
1683 "Expected cached manifest {}",
1684 ImageImporter::CACHED_KEY_MANIFEST
1685 )
1686 })?;
1687 let config = meta.lookup_value(ImageImporter::CACHED_KEY_CONFIG, None);
1688 let config: oci_image::ImageConfiguration = config
1689 .as_ref()
1690 .and_then(|v| v.str())
1691 .map(serde_json::from_str)
1692 .transpose()?
1693 .ok_or_else(|| {
1694 anyhow!(
1695 "Expected cached manifest {}",
1696 ImageImporter::CACHED_KEY_CONFIG
1697 )
1698 })?;
1699 Ok(Some(CachedImageUpdate {
1700 manifest,
1701 config,
1702 manifest_digest,
1703 }))
1704}
1705
1706#[context("Clearing cached update {imgref}")]
1708pub fn clear_cached_update(repo: &ostree::Repo, imgref: &ImageReference) -> Result<()> {
1709 let cancellable = gio::Cancellable::NONE;
1710 let ostree_ref = ref_for_image(imgref)?;
1711 let rev = repo.require_rev(&ostree_ref)?;
1712 let Some(commitmeta) = repo.read_commit_detached_metadata(&rev, cancellable)? else {
1713 return Ok(());
1714 };
1715
1716 let mut commitmeta: BTreeMap<String, glib::Variant> =
1718 BTreeMap::from_variant(&commitmeta).unwrap();
1719 let mut changed = false;
1720 for key in [
1721 ImageImporter::CACHED_KEY_CONFIG,
1722 ImageImporter::CACHED_KEY_MANIFEST,
1723 ImageImporter::CACHED_KEY_MANIFEST_DIGEST,
1724 ] {
1725 if commitmeta.remove(key).is_some() {
1726 changed = true;
1727 }
1728 }
1729 if !changed {
1730 return Ok(());
1731 }
1732 let commitmeta = glib::Variant::from(commitmeta);
1733 repo.write_commit_detached_metadata(&rev, Some(&commitmeta), cancellable)?;
1734 Ok(())
1735}
1736
1737pub fn query_image_commit(repo: &ostree::Repo, commit: &str) -> Result<Box<LayeredImageState>> {
1740 let merge_commit = commit.to_string();
1741 let merge_commit_obj = repo.load_commit(commit)?.0;
1742 let commit_meta = &merge_commit_obj.child_value(0);
1743 let commit_meta = &ostree::glib::VariantDict::new(Some(commit_meta));
1744 let (manifest, manifest_digest) = manifest_data_from_commitmeta(commit_meta)?;
1745 let configuration = image_config_from_commitmeta(commit_meta)?;
1746 let mut layers = manifest.layers().iter().cloned();
1747 let base_layer = layers.next().ok_or_else(|| anyhow!("No layers found"))?;
1749 let base_layer = query_layer(repo, base_layer)?;
1750 let ostree_ref = base_layer.ostree_ref.as_str();
1751 let base_commit = base_layer
1752 .commit
1753 .ok_or_else(|| anyhow!("Missing base image ref {ostree_ref}"))?;
1754
1755 let detached_commitmeta =
1756 repo.read_commit_detached_metadata(&merge_commit, gio::Cancellable::NONE)?;
1757 let detached_commitmeta = detached_commitmeta
1758 .as_ref()
1759 .map(|v| glib::VariantDict::new(Some(v)));
1760 let cached_update = detached_commitmeta
1761 .as_ref()
1762 .map(parse_cached_update)
1763 .transpose()?
1764 .flatten();
1765 let state = Box::new(LayeredImageState {
1766 base_commit,
1767 merge_commit,
1768 manifest_digest,
1769 manifest,
1770 configuration,
1771 cached_update,
1772 verify_text: None,
1774 filtered_files: None,
1775 });
1776 tracing::debug!("Wrote merge commit {}", state.merge_commit);
1777 Ok(state)
1778}
1779
1780fn manifest_for_image(repo: &ostree::Repo, imgref: &ImageReference) -> Result<ImageManifest> {
1781 let ostree_ref = ref_for_image(imgref)?;
1782 let rev = repo.require_rev(&ostree_ref)?;
1783 let (commit_obj, _) = repo.load_commit(rev.as_str())?;
1784 let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
1785 Ok(manifest_data_from_commitmeta(commit_meta)?.0)
1786}
1787
1788#[context("Copying image")]
1791pub async fn copy(
1792 src_repo: &ostree::Repo,
1793 src_imgref: &ImageReference,
1794 dest_repo: &ostree::Repo,
1795 dest_imgref: &ImageReference,
1796) -> Result<()> {
1797 let src_ostree_ref = ref_for_image(src_imgref)?;
1798 let src_commit = src_repo.require_rev(&src_ostree_ref)?;
1799 let manifest = manifest_for_image(src_repo, src_imgref)?;
1800 let layer_refs = manifest
1802 .layers()
1803 .iter()
1804 .map(ref_for_layer)
1805 .chain(std::iter::once(Ok(src_commit.to_string())));
1806 for ostree_ref in layer_refs {
1807 let ostree_ref = ostree_ref?;
1808 let src_repo = src_repo.clone();
1809 let dest_repo = dest_repo.clone();
1810 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| -> Result<_> {
1811 let cancellable = Some(cancellable);
1812 let srcfd = &format!("file:///proc/self/fd/{}", src_repo.dfd());
1813 let flags = ostree::RepoPullFlags::MIRROR;
1814 let opts = glib::VariantDict::new(None);
1815 let refs = [ostree_ref.as_str()];
1816 opts.insert("disable-verify-bindings", true);
1818 opts.insert("refs", &refs[..]);
1819 opts.insert("flags", flags.bits() as i32);
1820 let options = opts.to_variant();
1821 dest_repo.pull_with_options(srcfd, &options, None, cancellable)?;
1822 Ok(())
1823 })
1824 .await?;
1825 }
1826
1827 let dest_ostree_ref = ref_for_image(dest_imgref)?;
1828 dest_repo.set_ref_immediate(
1829 None,
1830 &dest_ostree_ref,
1831 Some(&src_commit),
1832 gio::Cancellable::NONE,
1833 )?;
1834
1835 Ok(())
1836}
1837
1838#[derive(Clone, Debug, Default)]
1840#[non_exhaustive]
1841pub struct ExportToOCIOpts {
1842 pub skip_compression: bool,
1844 pub authfile: Option<std::path::PathBuf>,
1846 pub progress_to_stdout: bool,
1848}
1849
1850fn chunking_from_layer_committed(
1855 repo: &ostree::Repo,
1856 l: &Descriptor,
1857 chunking: &mut chunking::Chunking,
1858) -> Result<()> {
1859 let mut chunk = Chunk::default();
1860 let layer_ref = &ref_for_layer(l)?;
1861 let root = repo.read_commit(layer_ref, gio::Cancellable::NONE)?.0;
1862 let e = root.enumerate_children(
1863 "standard::name,standard::size",
1864 gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS,
1865 gio::Cancellable::NONE,
1866 )?;
1867 for child in e.clone() {
1868 let child = &child?;
1869 let name = child.name();
1871 let name = Utf8Path::from_path(&name).unwrap();
1873 ostree::validate_checksum_string(name.as_str())?;
1874 chunking.remainder.move_obj(&mut chunk, name.as_str());
1875 }
1876 chunking.chunks.push(chunk);
1877 Ok(())
1878}
1879
1880#[context("Copying image")]
1882pub(crate) fn export_to_oci(
1883 repo: &ostree::Repo,
1884 imgref: &ImageReference,
1885 dest_oci: &Dir,
1886 tag: Option<&str>,
1887 opts: ExportToOCIOpts,
1888) -> Result<Descriptor> {
1889 let srcinfo = query_image(repo, imgref)?.ok_or_else(|| anyhow!("No such image"))?;
1890 let (commit_layer, component_layers, remaining_layers) =
1891 parse_manifest_layout(&srcinfo.manifest, &srcinfo.configuration)?;
1892
1893 let mut new_manifest = srcinfo.manifest.clone();
1897 new_manifest.layers_mut().clear();
1898 let mut new_config = srcinfo.configuration.clone();
1899 if let Some(history) = new_config.history_mut() {
1900 history.clear();
1901 }
1902 new_config.rootfs_mut().diff_ids_mut().clear();
1903
1904 let opts = ExportOpts {
1905 skip_compression: opts.skip_compression,
1906 authfile: opts.authfile,
1907 ..Default::default()
1908 };
1909
1910 let mut labels = HashMap::new();
1911
1912 let mut dest_oci = ocidir::OciDir::ensure(dest_oci.try_clone()?)?;
1913
1914 let commit_chunk_ref = commit_layer
1915 .as_ref()
1916 .map(|l| ref_for_layer(l))
1917 .transpose()?;
1918 let commit_chunk_rev = commit_chunk_ref
1919 .as_ref()
1920 .map(|r| repo.require_rev(&r))
1921 .transpose()?;
1922 if let Some(commit_chunk_rev) = commit_chunk_rev {
1923 let mut chunking = chunking::Chunking::new(repo, &commit_chunk_rev)?;
1924 for layer in component_layers {
1925 chunking_from_layer_committed(repo, layer, &mut chunking)?;
1926 }
1927
1928 export_chunked(
1931 repo,
1932 &srcinfo.base_commit,
1933 &mut dest_oci,
1934 &mut new_manifest,
1935 &mut new_config,
1936 &mut labels,
1937 chunking,
1938 &opts,
1939 "",
1940 )?;
1941 }
1942
1943 let compression = opts.skip_compression.then_some(Compression::none());
1945 for (i, layer) in remaining_layers.iter().enumerate() {
1946 let layer_ref = &ref_for_layer(layer)?;
1947 let mut target_blob = dest_oci.create_gzip_layer(compression)?;
1948 let export_opts = crate::tar::ExportOptions { raw: true };
1950 crate::tar::export_commit(
1951 repo,
1952 layer_ref.as_str(),
1953 &mut target_blob,
1954 Some(export_opts),
1955 )?;
1956 let layer = target_blob.complete()?;
1957 let previous_annotations = srcinfo
1958 .manifest
1959 .layers()
1960 .get(i)
1961 .and_then(|l| l.annotations().as_ref())
1962 .cloned();
1963 let history = srcinfo.configuration.history().as_ref();
1964 let history_entry = history.and_then(|v| v.get(i));
1965 let previous_description = history_entry
1966 .clone()
1967 .and_then(|h| h.comment().as_deref())
1968 .unwrap_or_default();
1969
1970 let previous_created = history_entry
1971 .and_then(|h| h.created().as_deref())
1972 .and_then(bootc_utils::try_deserialize_timestamp)
1973 .unwrap_or_default();
1974
1975 dest_oci.push_layer_full(
1976 &mut new_manifest,
1977 &mut new_config,
1978 layer,
1979 previous_annotations,
1980 previous_description,
1981 previous_created,
1982 )
1983 }
1984
1985 let new_config = dest_oci.write_config(new_config)?;
1986 new_manifest.set_config(new_config);
1987
1988 Ok(dest_oci.insert_manifest(new_manifest, tag, oci_image::Platform::default())?)
1989}
1990
1991#[context("Export")]
1994pub async fn export(
1995 repo: &ostree::Repo,
1996 src_imgref: &ImageReference,
1997 dest_imgref: &ImageReference,
1998 opts: Option<ExportToOCIOpts>,
1999) -> Result<oci_image::Digest> {
2000 let opts = opts.unwrap_or_default();
2001 let target_oci = dest_imgref.transport == Transport::OciDir;
2002 let tempdir = if !target_oci {
2003 let vartmp = cap_std::fs::Dir::open_ambient_dir("/var/tmp", cap_std::ambient_authority())?;
2004 let td = cap_std_ext::cap_tempfile::TempDir::new_in(&vartmp)?;
2005 let opts = ExportToOCIOpts {
2007 skip_compression: true,
2008 progress_to_stdout: opts.progress_to_stdout,
2009 ..Default::default()
2010 };
2011 export_to_oci(repo, src_imgref, &td, None, opts)?;
2012 td
2013 } else {
2014 let (path, tag) = parse_oci_path_and_tag(dest_imgref.name.as_str());
2015 tracing::debug!("using OCI path={path} tag={tag:?}");
2016 let path = Dir::open_ambient_dir(path, cap_std::ambient_authority())
2017 .with_context(|| format!("Opening {path}"))?;
2018 let descriptor = export_to_oci(repo, src_imgref, &path, tag, opts)?;
2019 return Ok(descriptor.digest().clone());
2020 };
2021 let target_fd = 3i32;
2023 let tempoci = ImageReference {
2024 transport: Transport::OciDir,
2025 name: format!("/proc/self/fd/{target_fd}"),
2026 };
2027 let authfile = opts.authfile.as_deref();
2028 skopeo::copy(
2029 &tempoci,
2030 dest_imgref,
2031 authfile,
2032 Some((std::sync::Arc::new(tempdir.try_clone()?.into()), target_fd)),
2033 opts.progress_to_stdout,
2034 )
2035 .await
2036}
2037
2038#[context("Listing deployment manifests")]
2041fn list_container_deployment_manifests(
2042 repo: &ostree::Repo,
2043 cancellable: Option<&gio::Cancellable>,
2044) -> Result<Vec<ImageManifest>> {
2045 let commits = OSTREE_BASE_DEPLOYMENT_REFS
2048 .iter()
2049 .chain(RPMOSTREE_BASE_REFS)
2050 .chain(std::iter::once(&BASE_IMAGE_PREFIX))
2051 .try_fold(
2052 std::collections::HashSet::new(),
2053 |mut acc, &p| -> Result<_> {
2054 let refs = repo.list_refs_ext(
2055 Some(p),
2056 ostree::RepoListRefsExtFlags::empty(),
2057 cancellable,
2058 )?;
2059 for (_, v) in refs {
2060 acc.insert(v);
2061 }
2062 Ok(acc)
2063 },
2064 )?;
2065 let mut r = Vec::new();
2067 for commit in commits {
2068 let commit_obj = repo.load_commit(&commit)?.0;
2069 let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
2070 if commit_meta
2071 .lookup::<String>(META_MANIFEST_DIGEST)?
2072 .is_some()
2073 {
2074 tracing::trace!("Commit {commit} is a container image");
2075 let manifest = manifest_data_from_commitmeta(commit_meta)?.0;
2076 r.push(manifest);
2077 }
2078 }
2079 Ok(r)
2080}
2081
2082pub fn gc_image_layers(repo: &ostree::Repo) -> Result<u32> {
2088 gc_image_layers_impl(repo, gio::Cancellable::NONE)
2089}
2090
2091#[context("Pruning image layers")]
2092fn gc_image_layers_impl(
2093 repo: &ostree::Repo,
2094 cancellable: Option<&gio::Cancellable>,
2095) -> Result<u32> {
2096 let all_images = list_images(repo)?;
2097 let deployment_commits = list_container_deployment_manifests(repo, cancellable)?;
2098 let all_manifests = all_images
2099 .into_iter()
2100 .map(|img| -> Result<_> {
2101 let ir = ImageReference::try_from(img.as_str())?;
2102 manifest_for_image(repo, &ir)
2103 })
2104 .chain(deployment_commits.into_iter().map(Ok))
2105 .collect::<Result<Vec<_>>>()?;
2106 tracing::debug!("Images found: {}", all_manifests.len());
2107 let mut referenced_layers = BTreeSet::new();
2108 for m in all_manifests.iter() {
2109 for layer in m.layers() {
2110 referenced_layers.insert(layer.digest().to_string());
2111 }
2112 }
2113 tracing::debug!("Referenced layers: {}", referenced_layers.len());
2114 let found_layers = repo
2115 .list_refs_ext(
2116 Some(LAYER_PREFIX),
2117 ostree::RepoListRefsExtFlags::empty(),
2118 cancellable,
2119 )?
2120 .into_iter()
2121 .map(|v| v.0);
2122 tracing::debug!("Found layers: {}", found_layers.len());
2123 let mut pruned = 0u32;
2124 for layer_ref in found_layers {
2125 let layer_digest = refescape::unprefix_unescape_ref(LAYER_PREFIX, &layer_ref)?;
2126 if referenced_layers.remove(layer_digest.as_str()) {
2127 continue;
2128 }
2129 pruned += 1;
2130 tracing::debug!("Pruning: {}", layer_ref.as_str());
2131 repo.set_ref_immediate(None, layer_ref.as_str(), None, cancellable)?;
2132 }
2133
2134 Ok(pruned)
2135}
2136
2137#[cfg(feature = "internal-testing-api")]
2138pub fn count_layer_references(repo: &ostree::Repo) -> Result<u32> {
2140 let cancellable = gio::Cancellable::NONE;
2141 let n = repo
2142 .list_refs_ext(
2143 Some(LAYER_PREFIX),
2144 ostree::RepoListRefsExtFlags::empty(),
2145 cancellable,
2146 )?
2147 .len();
2148 Ok(n as u32)
2149}
2150
2151pub fn image_filtered_content_warning(
2153 filtered_files: &Option<MetaFilteredData>,
2154) -> Result<Option<String>> {
2155 use std::fmt::Write;
2156
2157 let r = filtered_files.as_ref().map(|v| {
2158 let mut filtered = BTreeMap::<&String, u32>::new();
2159 for paths in v.values() {
2160 for (k, v) in paths {
2161 let e = filtered.entry(k).or_default();
2162 *e += v;
2163 }
2164 }
2165 let mut buf = "Image contains non-ostree compatible file paths:".to_string();
2166 for (k, v) in filtered {
2167 write!(buf, " {k}: {v}").unwrap();
2168 }
2169 buf
2170 });
2171 Ok(r)
2172}
2173
2174#[context("Pruning {img}")]
2181pub fn remove_image(repo: &ostree::Repo, img: &ImageReference) -> Result<bool> {
2182 let ostree_ref = &ref_for_image(img)?;
2183 let found = repo.resolve_rev(ostree_ref, true)?.is_some();
2184 if found {
2187 repo.set_ref_immediate(None, ostree_ref, None, gio::Cancellable::NONE)?;
2188 }
2189 Ok(found)
2190}
2191
2192pub fn remove_images<'a>(
2199 repo: &ostree::Repo,
2200 imgs: impl IntoIterator<Item = &'a ImageReference>,
2201) -> Result<()> {
2202 let mut missing = Vec::new();
2203 for img in imgs.into_iter() {
2204 let found = remove_image(repo, img)?;
2205 if !found {
2206 missing.push(img);
2207 }
2208 }
2209 if !missing.is_empty() {
2210 let missing = missing.into_iter().fold("".to_string(), |mut a, v| {
2211 a.push_str(&v.to_string());
2212 a
2213 });
2214 return Err(anyhow::anyhow!("Missing images: {missing}"));
2215 }
2216 Ok(())
2217}
2218
2219#[derive(Debug, Default)]
2220struct CompareState {
2221 verified: BTreeSet<Utf8PathBuf>,
2222 inode_corrupted: BTreeSet<Utf8PathBuf>,
2223 unknown_corrupted: BTreeSet<Utf8PathBuf>,
2224}
2225
2226impl CompareState {
2227 fn is_ok(&self) -> bool {
2228 self.inode_corrupted.is_empty() && self.unknown_corrupted.is_empty()
2229 }
2230}
2231
2232fn compare_file_info(src: &gio::FileInfo, target: &gio::FileInfo) -> bool {
2233 if src.file_type() != target.file_type() {
2234 return false;
2235 }
2236 if src.size() != target.size() {
2237 return false;
2238 }
2239 for attr in ["unix::uid", "unix::gid", "unix::mode"] {
2240 if src.attribute_uint32(attr) != target.attribute_uint32(attr) {
2241 return false;
2242 }
2243 }
2244 true
2245}
2246
2247#[context("Querying object inode")]
2248fn inode_of_object(repo: &ostree::Repo, checksum: &str) -> Result<u64> {
2249 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
2250 let (prefix, suffix) = checksum.split_at(2);
2251 let objpath = format!("objects/{prefix}/{suffix}.file");
2252 let metadata = repodir.symlink_metadata(objpath)?;
2253 Ok(metadata.ino())
2254}
2255
2256fn compare_commit_trees(
2257 repo: &ostree::Repo,
2258 root: &Utf8Path,
2259 target: &ostree::RepoFile,
2260 expected: &ostree::RepoFile,
2261 exact: bool,
2262 colliding_inodes: &BTreeSet<u64>,
2263 state: &mut CompareState,
2264) -> Result<()> {
2265 let cancellable = gio::Cancellable::NONE;
2266 let queryattrs = "standard::name,standard::type";
2267 let queryflags = gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS;
2268 let expected_iter = expected.enumerate_children(queryattrs, queryflags, cancellable)?;
2269
2270 while let Some(expected_info) = expected_iter.next_file(cancellable)? {
2271 let expected_child = expected_iter.child(&expected_info);
2272 let name = expected_info.name();
2273 let name = name.to_str().expect("UTF-8 ostree name");
2274 let path = Utf8PathBuf::from(format!("{root}{name}"));
2275 let target_child = target.child(name);
2276 let target_info = crate::diff::query_info_optional(&target_child, queryattrs, queryflags)
2277 .context("querying optional to")?;
2278 let is_dir = matches!(expected_info.file_type(), gio::FileType::Directory);
2279 if let Some(target_info) = target_info {
2280 let to_child = target_child
2281 .downcast::<ostree::RepoFile>()
2282 .expect("downcast");
2283 to_child.ensure_resolved()?;
2284 let from_child = expected_child
2285 .downcast::<ostree::RepoFile>()
2286 .expect("downcast");
2287 from_child.ensure_resolved()?;
2288
2289 if is_dir {
2290 let from_contents_checksum = from_child.tree_get_contents_checksum();
2291 let to_contents_checksum = to_child.tree_get_contents_checksum();
2292 if from_contents_checksum != to_contents_checksum {
2293 let subpath = Utf8PathBuf::from(format!("{path}/"));
2294 compare_commit_trees(
2295 repo,
2296 &subpath,
2297 &from_child,
2298 &to_child,
2299 exact,
2300 colliding_inodes,
2301 state,
2302 )?;
2303 }
2304 } else {
2305 let from_checksum = from_child.checksum();
2306 let to_checksum = to_child.checksum();
2307 let matches = if exact {
2308 from_checksum == to_checksum
2309 } else {
2310 compare_file_info(&target_info, &expected_info)
2311 };
2312 if !matches {
2313 let from_inode = inode_of_object(repo, &from_checksum)?;
2314 let to_inode = inode_of_object(repo, &to_checksum)?;
2315 if colliding_inodes.contains(&from_inode)
2316 || colliding_inodes.contains(&to_inode)
2317 {
2318 state.inode_corrupted.insert(path);
2319 } else {
2320 state.unknown_corrupted.insert(path);
2321 }
2322 } else {
2323 state.verified.insert(path);
2324 }
2325 }
2326 } else {
2327 eprintln!("Missing {path}");
2328 state.unknown_corrupted.insert(path);
2329 }
2330 }
2331 Ok(())
2332}
2333
2334#[context("Verifying container image state")]
2335pub(crate) fn verify_container_image(
2336 sysroot: &SysrootLock,
2337 imgref: &ImageReference,
2338 state: &LayeredImageState,
2339 colliding_inodes: &BTreeSet<u64>,
2340 verbose: bool,
2341) -> Result<bool> {
2342 let cancellable = gio::Cancellable::NONE;
2343 let repo = &sysroot.repo();
2344 let merge_commit = state.merge_commit.as_str();
2345 let merge_commit_root = repo.read_commit(merge_commit, gio::Cancellable::NONE)?.0;
2346 let merge_commit_root = merge_commit_root
2347 .downcast::<ostree::RepoFile>()
2348 .expect("downcast");
2349 merge_commit_root.ensure_resolved()?;
2350
2351 let (commit_layer, _component_layers, remaining_layers) =
2352 parse_manifest_layout(&state.manifest, &state.configuration)?;
2353
2354 let mut comparison_state = CompareState::default();
2355
2356 let query = |l: &Descriptor| query_layer(repo, l.clone());
2357
2358 let base_tree = repo
2359 .read_commit(&state.base_commit, cancellable)?
2360 .0
2361 .downcast::<ostree::RepoFile>()
2362 .expect("downcast");
2363 if let Some(commit_layer) = commit_layer {
2364 println!(
2365 "Verifying with base ostree layer {}",
2366 ref_for_layer(commit_layer)?
2367 );
2368 }
2369 compare_commit_trees(
2370 repo,
2371 "/".into(),
2372 &merge_commit_root,
2373 &base_tree,
2374 true,
2375 colliding_inodes,
2376 &mut comparison_state,
2377 )?;
2378
2379 let remaining_layers = remaining_layers
2380 .into_iter()
2381 .map(query)
2382 .collect::<Result<Vec<_>>>()?;
2383
2384 println!("Image has {} derived layers", remaining_layers.len());
2385
2386 for layer in remaining_layers.iter().rev() {
2387 let layer_ref = layer.ostree_ref.as_str();
2388 let layer_commit = layer
2389 .commit
2390 .as_deref()
2391 .ok_or_else(|| anyhow!("Missing layer {layer_ref}"))?;
2392 let layer_tree = repo
2393 .read_commit(layer_commit, cancellable)?
2394 .0
2395 .downcast::<ostree::RepoFile>()
2396 .expect("downcast");
2397 compare_commit_trees(
2398 repo,
2399 "/".into(),
2400 &merge_commit_root,
2401 &layer_tree,
2402 false,
2403 colliding_inodes,
2404 &mut comparison_state,
2405 )?;
2406 }
2407
2408 let n_verified = comparison_state.verified.len();
2409 if comparison_state.is_ok() {
2410 println!("OK image {imgref} (verified={n_verified})");
2411 println!();
2412 } else {
2413 let n_inode = comparison_state.inode_corrupted.len();
2414 let n_other = comparison_state.unknown_corrupted.len();
2415 eprintln!("warning: Found corrupted merge commit");
2416 eprintln!(" inode clashes: {n_inode}");
2417 eprintln!(" unknown: {n_other}");
2418 eprintln!(" ok: {n_verified}");
2419 if verbose {
2420 eprintln!("Mismatches:");
2421 for path in comparison_state.inode_corrupted {
2422 eprintln!(" inode: {path}");
2423 }
2424 for path in comparison_state.unknown_corrupted {
2425 eprintln!(" other: {path}");
2426 }
2427 }
2428 eprintln!();
2429 return Ok(false);
2430 }
2431
2432 Ok(true)
2433}
2434
2435#[cfg(test)]
2436mod tests {
2437 use cap_std_ext::cap_tempfile;
2438 use oci_image::{DescriptorBuilder, MediaType, Sha256Digest};
2439
2440 use super::*;
2441
2442 #[test]
2443 fn test_ref_for_descriptor() {
2444 let d = DescriptorBuilder::default()
2445 .size(42u64)
2446 .media_type(MediaType::ImageManifest)
2447 .digest(
2448 Sha256Digest::from_str(
2449 "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
2450 )
2451 .unwrap(),
2452 )
2453 .build()
2454 .unwrap();
2455 assert_eq!(
2456 ref_for_layer(&d).unwrap(),
2457 "ostree/container/blob/sha256_3A_2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
2458 );
2459 }
2460
2461 #[test]
2462 fn test_cleanup_root() -> Result<()> {
2463 let td = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2464 let usretc = "usr/etc";
2465 cleanup_root(&td).unwrap();
2466 td.create_dir_all(usretc)?;
2467 let usretc = &td.open_dir(usretc)?;
2468 usretc.write("hostname", b"hostname")?;
2469 cleanup_root(&td).unwrap();
2470 assert!(usretc.try_exists("hostname")?);
2471 usretc.write("hostname", b"")?;
2472 cleanup_root(&td).unwrap();
2473 assert!(!td.try_exists("hostname")?);
2474
2475 usretc.symlink_contents("../run/systemd/stub-resolv.conf", "resolv.conf")?;
2476 cleanup_root(&td).unwrap();
2477 assert!(usretc.symlink_metadata("resolv.conf")?.is_symlink());
2478 usretc.remove_file("resolv.conf")?;
2479 usretc.write("resolv.conf", b"")?;
2480 cleanup_root(&td).unwrap();
2481 assert!(!usretc.try_exists("resolv.conf")?);
2482
2483 Ok(())
2484 }
2485}