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
295#[derive(Debug)]
297pub struct ImageImporter {
298 repo: ostree::Repo,
299 pub(crate) proxy: ImageProxy,
300 imgref: OstreeImageReference,
301 target_imgref: Option<OstreeImageReference>,
302 no_imgref: bool, disable_gc: bool, require_bootable: bool,
306 offline: bool,
308 ostree_v2024_3: bool,
310
311 layer_progress: Option<Sender<ImportProgress>>,
312 layer_byte_progress: Option<tokio::sync::watch::Sender<Option<LayerProgress>>>,
313}
314
315#[derive(Debug)]
317pub enum PrepareResult {
318 AlreadyPresent(Box<LayeredImageState>),
320 Ready(Box<PreparedImport>),
322}
323
324#[derive(Debug)]
326pub struct ManifestLayerState {
327 pub layer: oci_image::Descriptor,
329 pub ostree_ref: String,
332 pub commit: Option<String>,
335}
336
337impl ManifestLayerState {
338 pub fn layer(&self) -> &oci_image::Descriptor {
340 &self.layer
341 }
342}
343
344#[derive(Debug)]
346pub struct PreparedImport {
347 pub manifest_digest: Digest,
349 pub manifest: oci_image::ImageManifest,
351 pub config: oci_image::ImageConfiguration,
353 pub previous_state: Option<Box<LayeredImageState>>,
355 pub previous_manifest_digest: Option<Digest>,
357 pub previous_imageid: Option<String>,
359 pub ostree_layers: Vec<ManifestLayerState>,
361 pub ostree_commit_layer: Option<ManifestLayerState>,
363 pub layers: Vec<ManifestLayerState>,
365 pub verify_text: Option<String>,
367 proxy_img: OpenedImage,
369}
370
371impl PreparedImport {
372 pub fn all_layers(&self) -> impl Iterator<Item = &ManifestLayerState> {
374 self.ostree_commit_layer
375 .iter()
376 .chain(self.ostree_layers.iter())
377 .chain(self.layers.iter())
378 }
379
380 pub fn version(&self) -> Option<&str> {
382 super::version_for_config(&self.config)
383 }
384
385 pub fn deprecated_warning(&self) -> Option<&'static str> {
387 None
388 }
389
390 pub fn layers_with_history(
393 &self,
394 ) -> impl Iterator<Item = Result<(&ManifestLayerState, &History)>> {
395 let truncated = std::iter::once_with(|| Err(anyhow::anyhow!("Truncated history")));
397 let history = self
398 .config
399 .history()
400 .iter()
401 .flatten()
402 .map(Ok)
403 .chain(truncated);
404 self.all_layers()
405 .zip(history)
406 .map(|(s, h)| h.map(|h| (s, h)))
407 }
408
409 pub fn layers_to_fetch(&self) -> impl Iterator<Item = Result<(&ManifestLayerState, &str)>> {
411 self.layers_with_history().filter_map(|r| {
412 r.map(|(l, h)| {
413 l.commit.is_none().then(|| {
414 let comment = h.created_by().as_deref().unwrap_or("");
415 (l, comment)
416 })
417 })
418 .transpose()
419 })
420 }
421
422 pub(crate) fn format_layer_status(&self) -> Option<String> {
424 let (stored, to_fetch, to_fetch_size) =
425 self.all_layers()
426 .fold((0u32, 0u32, 0u64), |(stored, to_fetch, sz), v| {
427 if v.commit.is_some() {
428 (stored + 1, to_fetch, sz)
429 } else {
430 (stored, to_fetch + 1, sz + v.layer().size())
431 }
432 });
433 (to_fetch > 0).then(|| {
434 let size = crate::glib::format_size(to_fetch_size);
435 format!("layers already present: {stored}; layers needed: {to_fetch} ({size})")
436 })
437 }
438}
439
440pub(crate) fn query_layer(
442 repo: &ostree::Repo,
443 layer: oci_image::Descriptor,
444) -> Result<ManifestLayerState> {
445 let ostree_ref = ref_for_layer(&layer)?;
446 let commit = repo.resolve_rev(&ostree_ref, true)?.map(|s| s.to_string());
447 Ok(ManifestLayerState {
448 layer,
449 ostree_ref,
450 commit,
451 })
452}
453
454#[context("Reading manifest data from commit")]
455fn manifest_data_from_commitmeta(
456 commit_meta: &glib::VariantDict,
457) -> Result<(oci_image::ImageManifest, Digest)> {
458 let digest = commit_meta
459 .lookup::<String>(META_MANIFEST_DIGEST)?
460 .ok_or_else(|| anyhow!("Missing {} metadata on merge commit", META_MANIFEST_DIGEST))?;
461 let digest = Digest::from_str(&digest)?;
462 let manifest_bytes: String = commit_meta
463 .lookup::<String>(META_MANIFEST)?
464 .ok_or_else(|| anyhow!("Failed to find {} metadata key", META_MANIFEST))?;
465 let r = serde_json::from_str(&manifest_bytes)?;
466 Ok((r, digest))
467}
468
469fn image_config_from_commitmeta(commit_meta: &glib::VariantDict) -> Result<ImageConfiguration> {
470 let config = if let Some(config) = commit_meta
471 .lookup::<String>(META_CONFIG)?
472 .filter(|v| v != "null") .map(|v| serde_json::from_str(&v).map_err(anyhow::Error::msg))
474 .transpose()?
475 {
476 config
477 } else {
478 tracing::debug!("No image configuration found");
479 Default::default()
480 };
481 Ok(config)
482}
483
484pub fn manifest_digest_from_commit(commit: &glib::Variant) -> Result<Digest> {
490 let commit_meta = &commit.child_value(0);
491 let commit_meta = &glib::VariantDict::new(Some(commit_meta));
492 Ok(manifest_data_from_commitmeta(commit_meta)?.1)
493}
494
495fn layer_from_diffid<'a>(
499 manifest: &'a ImageManifest,
500 config: &ImageConfiguration,
501 diffid: &str,
502) -> Result<&'a Descriptor> {
503 let idx = config
504 .rootfs()
505 .diff_ids()
506 .iter()
507 .position(|x| x.as_str() == diffid)
508 .ok_or_else(|| anyhow!("Missing {} {}", DIFFID_LABEL, diffid))?;
509 manifest.layers().get(idx).ok_or_else(|| {
510 anyhow!(
511 "diffid position {} exceeds layer count {}",
512 idx,
513 manifest.layers().len()
514 )
515 })
516}
517
518#[context("Parsing manifest layout")]
519pub(crate) fn parse_manifest_layout<'a>(
520 manifest: &'a ImageManifest,
521 config: &ImageConfiguration,
522) -> Result<(
523 Option<&'a Descriptor>,
524 Vec<&'a Descriptor>,
525 Vec<&'a Descriptor>,
526)> {
527 let config_labels = super::labels_of(config);
528
529 let first_layer = manifest
530 .layers()
531 .first()
532 .ok_or_else(|| anyhow!("No layers in manifest"))?;
533 let Some(target_diffid) = config_labels.and_then(|labels| labels.get(DIFFID_LABEL)) else {
534 return Ok((None, Vec::new(), manifest.layers().iter().collect()));
535 };
536
537 let target_layer = layer_from_diffid(manifest, config, target_diffid.as_str())?;
538 let mut chunk_layers = Vec::new();
539 let mut derived_layers = Vec::new();
540 let mut after_target = false;
541 let ostree_layer = first_layer;
543 for layer in manifest.layers() {
544 if layer == target_layer {
545 if after_target {
546 anyhow::bail!("Multiple entries for {}", layer.digest());
547 }
548 after_target = true;
549 if layer != ostree_layer {
550 chunk_layers.push(layer);
551 }
552 } else if !after_target {
553 if layer != ostree_layer {
554 chunk_layers.push(layer);
555 }
556 } else {
557 derived_layers.push(layer);
558 }
559 }
560
561 Ok((Some(ostree_layer), chunk_layers, derived_layers))
562}
563
564#[context("Parsing manifest layout")]
566pub(crate) fn parse_ostree_manifest_layout<'a>(
567 manifest: &'a ImageManifest,
568 config: &ImageConfiguration,
569) -> Result<(&'a Descriptor, Vec<&'a Descriptor>, Vec<&'a Descriptor>)> {
570 let (ostree_layer, component_layers, derived_layers) = parse_manifest_layout(manifest, config)?;
571 let ostree_layer = ostree_layer.ok_or_else(|| {
572 anyhow!("No {DIFFID_LABEL} label found, not an ostree encapsulated container")
573 })?;
574 Ok((ostree_layer, component_layers, derived_layers))
575}
576
577fn timestamp_of_manifest_or_config(
579 manifest: &ImageManifest,
580 config: &ImageConfiguration,
581) -> Option<u64> {
582 let timestamp = manifest
585 .annotations()
586 .as_ref()
587 .and_then(|a| a.get(oci_image::ANNOTATION_CREATED))
588 .or_else(|| config.created().as_ref());
589 timestamp
591 .map(|t| {
592 chrono::DateTime::parse_from_rfc3339(t)
593 .context("Failed to parse manifest timestamp")
594 .map(|t| t.timestamp() as u64)
595 })
596 .transpose()
597 .log_err_default()
598}
599
600fn cleanup_root(root: &Dir) -> Result<()> {
603 const RUNTIME_INJECTED: &[&str] = &["usr/etc/hostname", "usr/etc/resolv.conf"];
604 for ent in RUNTIME_INJECTED {
605 if let Some(meta) = root.symlink_metadata_optional(ent)? {
606 if meta.is_file() && meta.size() == 0 {
607 tracing::debug!("Removing {ent}");
608 root.remove_file(ent)?;
609 }
610 }
611 }
612 Ok(())
613}
614
615impl ImageImporter {
616 const CACHED_KEY_MANIFEST_DIGEST: &'static str = "ostree-ext.cached.manifest-digest";
618 const CACHED_KEY_MANIFEST: &'static str = "ostree-ext.cached.manifest";
619 const CACHED_KEY_CONFIG: &'static str = "ostree-ext.cached.config";
620
621 #[context("Creating importer")]
623 pub async fn new(
624 repo: &ostree::Repo,
625 imgref: &OstreeImageReference,
626 mut config: ImageProxyConfig,
627 ) -> Result<Self> {
628 if imgref.imgref.transport == Transport::ContainerStorage {
629 merge_default_container_proxy_opts_with_isolation(&mut config, None)?;
631 } else {
632 merge_default_container_proxy_opts(&mut config)?;
634 }
635 let proxy = ImageProxy::new_with_config(config).await?;
636
637 system_repo_journal_print(
638 repo,
639 libsystemd::logging::Priority::Info,
640 &format!("Fetching {imgref}"),
641 );
642
643 let repo = repo.clone();
644 Ok(ImageImporter {
645 repo,
646 proxy,
647 target_imgref: None,
648 no_imgref: false,
649 ostree_v2024_3: ostree::check_version(2024, 3),
650 disable_gc: false,
651 require_bootable: false,
652 offline: false,
653 imgref: imgref.clone(),
654 layer_progress: None,
655 layer_byte_progress: None,
656 })
657 }
658
659 pub fn set_target(&mut self, target: &OstreeImageReference) {
661 self.target_imgref = Some(target.clone())
662 }
663
664 pub fn set_no_imgref(&mut self) {
668 self.no_imgref = true;
669 }
670
671 pub fn set_offline(&mut self) {
673 self.offline = true;
674 }
675
676 pub fn require_bootable(&mut self) {
678 self.require_bootable = true;
679 }
680
681 pub fn set_ostree_version(&mut self, year: u32, v: u32) {
683 self.ostree_v2024_3 = (year > 2024) || (year == 2024 && v >= 3)
684 }
685
686 pub fn disable_gc(&mut self) {
688 self.disable_gc = true;
689 }
690
691 #[context("Preparing import")]
696 pub async fn prepare(&mut self) -> Result<PrepareResult> {
697 self.prepare_internal(false).await
698 }
699
700 pub fn request_progress(&mut self) -> Receiver<ImportProgress> {
702 assert!(self.layer_progress.is_none());
703 let (s, r) = tokio::sync::mpsc::channel(2);
704 self.layer_progress = Some(s);
705 r
706 }
707
708 pub fn request_layer_progress(
710 &mut self,
711 ) -> tokio::sync::watch::Receiver<Option<LayerProgress>> {
712 assert!(self.layer_byte_progress.is_none());
713 let (s, r) = tokio::sync::watch::channel(None);
714 self.layer_byte_progress = Some(s);
715 r
716 }
717
718 #[context("Writing cached pending manifest")]
721 pub(crate) async fn cache_pending(
722 &self,
723 commit: &str,
724 manifest_digest: &Digest,
725 manifest: &ImageManifest,
726 config: &ImageConfiguration,
727 ) -> Result<()> {
728 let commitmeta = glib::VariantDict::new(None);
729 commitmeta.insert(
730 Self::CACHED_KEY_MANIFEST_DIGEST,
731 manifest_digest.to_string(),
732 );
733 let cached_manifest = manifest
734 .to_canon_json_string()
735 .context("Serializing manifest")?;
736 commitmeta.insert(Self::CACHED_KEY_MANIFEST, cached_manifest);
737 let cached_config = config
738 .to_canon_json_string()
739 .context("Serializing config")?;
740 commitmeta.insert(Self::CACHED_KEY_CONFIG, cached_config);
741 let commitmeta = commitmeta.to_variant();
742 let commit = commit.to_string();
744 let repo = self.repo.clone();
745 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
746 repo.write_commit_detached_metadata(&commit, Some(&commitmeta), Some(cancellable))
747 .map_err(anyhow::Error::msg)
748 })
749 .await
750 }
751
752 fn create_prepared_import(
755 &mut self,
756 manifest_digest: Digest,
757 manifest: ImageManifest,
758 config: ImageConfiguration,
759 previous_state: Option<Box<LayeredImageState>>,
760 previous_imageid: Option<String>,
761 proxy_img: OpenedImage,
762 ) -> Result<Box<PreparedImport>> {
763 let config_labels = super::labels_of(&config);
764 if self.require_bootable {
765 let bootable_key = ostree::METADATA_KEY_BOOTABLE;
766 let bootable = config_labels.is_some_and(|l| {
767 l.contains_key(bootable_key.as_str()) || l.contains_key(BOOTC_LABEL)
768 });
769 if !bootable {
770 anyhow::bail!("Target image does not have {bootable_key} label");
771 }
772 let container_arch = config.architecture();
773 let target_arch = &Arch::default();
774 if container_arch != target_arch {
775 anyhow::bail!("Image has architecture {container_arch}; expected {target_arch}");
776 }
777 }
778
779 let (commit_layer, component_layers, remaining_layers) =
780 parse_manifest_layout(&manifest, &config)?;
781
782 let query = |l: &Descriptor| query_layer(&self.repo, l.clone());
783 let commit_layer = commit_layer.map(query).transpose()?;
784 let component_layers = component_layers
785 .into_iter()
786 .map(query)
787 .collect::<Result<Vec<_>>>()?;
788 let remaining_layers = remaining_layers
789 .into_iter()
790 .map(query)
791 .collect::<Result<Vec<_>>>()?;
792
793 let previous_manifest_digest = previous_state.as_ref().map(|s| s.manifest_digest.clone());
794 let imp = PreparedImport {
795 manifest_digest,
796 manifest,
797 config,
798 previous_state,
799 previous_manifest_digest,
800 previous_imageid,
801 ostree_layers: component_layers,
802 ostree_commit_layer: commit_layer,
803 layers: remaining_layers,
804 verify_text: None,
805 proxy_img,
806 };
807 Ok(Box::new(imp))
808 }
809
810 #[context("Fetching manifest")]
812 pub(crate) async fn prepare_internal(&mut self, verify_layers: bool) -> Result<PrepareResult> {
813 match &self.imgref.sigverify {
814 SignatureSource::ContainerPolicy if skopeo::container_policy_is_default_insecure()? => {
815 return Err(anyhow!(
816 "containers-policy.json specifies a default of `insecureAcceptAnything`; refusing usage"
817 ));
818 }
819 SignatureSource::OstreeRemote(_) if verify_layers => {
820 return Err(anyhow!(
821 "Cannot currently verify layered containers via ostree remote"
822 ));
823 }
824 _ => {}
825 }
826
827 let previous_state = try_query_image(&self.repo, &self.imgref.imgref)?;
829
830 let target_reference = self.imgref.imgref.name.parse::<Reference>().ok();
832 let previous_state = if let Some(target_digest) = target_reference
833 .as_ref()
834 .and_then(|v| v.digest())
835 .map(Digest::from_str)
836 .transpose()?
837 {
838 if let Some(previous_state) = previous_state {
839 if previous_state.manifest_digest == target_digest {
841 tracing::debug!("Digest-based pullspec {:?} already present", self.imgref);
842 return Ok(PrepareResult::AlreadyPresent(previous_state));
843 }
844 Some(previous_state)
845 } else {
846 None
847 }
848 } else {
849 previous_state
850 };
851
852 if self.offline {
853 anyhow::bail!("Manifest fetch required in offline mode");
854 }
855
856 let proxy_img = self
857 .proxy
858 .open_image(&self.imgref.imgref.to_string())
859 .await?;
860
861 let (manifest_digest, manifest) = self.proxy.fetch_manifest(&proxy_img).await?;
862 let manifest_digest = Digest::from_str(&manifest_digest)?;
863 let new_imageid = manifest.config().digest();
864
865 let (previous_state, previous_imageid) = if let Some(previous_state) = previous_state {
868 if previous_state.manifest_digest == manifest_digest {
870 return Ok(PrepareResult::AlreadyPresent(previous_state));
871 }
872 let previous_imageid = previous_state.manifest.config().digest();
874 if previous_imageid == new_imageid {
875 return Ok(PrepareResult::AlreadyPresent(previous_state));
876 }
877 let previous_imageid = previous_imageid.to_string();
878 (Some(previous_state), Some(previous_imageid))
879 } else {
880 (None, None)
881 };
882
883 let config = self.proxy.fetch_config(&proxy_img).await?;
884
885 if let Some(previous_state) = previous_state.as_ref() {
888 self.cache_pending(
889 previous_state.merge_commit.as_str(),
890 &manifest_digest,
891 &manifest,
892 &config,
893 )
894 .await?;
895 }
896
897 let imp = self.create_prepared_import(
898 manifest_digest,
899 manifest,
900 config,
901 previous_state,
902 previous_imageid,
903 proxy_img,
904 )?;
905 Ok(PrepareResult::Ready(imp))
906 }
907
908 #[context("Unencapsulating base")]
910 pub(crate) async fn unencapsulate_base(
911 &self,
912 import: &mut store::PreparedImport,
913 require_ostree: bool,
914 write_refs: bool,
915 ) -> Result<()> {
916 tracing::debug!("Fetching base");
917 if matches!(self.imgref.sigverify, SignatureSource::ContainerPolicy)
918 && skopeo::container_policy_is_default_insecure()?
919 {
920 return Err(anyhow!(
921 "containers-policy.json specifies a default of `insecureAcceptAnything`; refusing usage"
922 ));
923 }
924 let remote = match &self.imgref.sigverify {
925 SignatureSource::OstreeRemote(remote) => Some(remote.clone()),
926 SignatureSource::ContainerPolicy | SignatureSource::ContainerPolicyAllowInsecure => {
927 None
928 }
929 };
930 let Some(commit_layer) = import.ostree_commit_layer.as_mut() else {
931 if require_ostree {
932 anyhow::bail!(
933 "No {DIFFID_LABEL} label found, not an ostree encapsulated container"
934 );
935 }
936 return Ok(());
937 };
938 let des_layers = self.proxy.get_layer_info(&import.proxy_img).await?;
939 for layer in import.ostree_layers.iter_mut() {
940 if layer.commit.is_some() {
941 continue;
942 }
943 if let Some(p) = self.layer_progress.as_ref() {
944 p.send(ImportProgress::OstreeChunkStarted(layer.layer.clone()))
945 .await?;
946 }
947 let (blob, driver, media_type) = fetch_layer(
948 &self.proxy,
949 &import.proxy_img,
950 &import.manifest,
951 &layer.layer,
952 self.layer_byte_progress.as_ref(),
953 des_layers.as_ref(),
954 self.imgref.imgref.transport,
955 )
956 .await?;
957 let repo = self.repo.clone();
958 let target_ref = layer.ostree_ref.clone();
959 let import_task =
960 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
961 let txn = repo.auto_transaction(Some(cancellable))?;
962 let mut importer = crate::tar::Importer::new_for_object_set(&repo);
963 let blob = tokio_util::io::SyncIoBridge::new(blob);
964 let mut blob = Decompressor::new(&media_type, blob)?;
965 let mut archive = tar::Archive::new(&mut blob);
966 importer.import_objects(&mut archive, Some(cancellable))?;
967 let commit = if write_refs {
968 let commit = importer.finish_import_object_set()?;
969 repo.transaction_set_ref(None, &target_ref, Some(commit.as_str()));
970 tracing::debug!("Wrote {} => {}", target_ref, commit);
971 Some(commit)
972 } else {
973 None
974 };
975 txn.commit(Some(cancellable))?;
976 blob.finish()?;
977 Ok::<_, anyhow::Error>(commit)
978 })
979 .map_err(|e| e.context(format!("Layer {}", layer.layer.digest())));
980 let commit = super::unencapsulate::join_fetch(import_task, driver).await?;
981 layer.commit = commit;
982 if let Some(p) = self.layer_progress.as_ref() {
983 p.send(ImportProgress::OstreeChunkCompleted(layer.layer.clone()))
984 .await?;
985 }
986 }
987 if commit_layer.commit.is_none() {
988 if let Some(p) = self.layer_progress.as_ref() {
989 p.send(ImportProgress::OstreeChunkStarted(
990 commit_layer.layer.clone(),
991 ))
992 .await?;
993 }
994 let (blob, driver, media_type) = fetch_layer(
995 &self.proxy,
996 &import.proxy_img,
997 &import.manifest,
998 &commit_layer.layer,
999 self.layer_byte_progress.as_ref(),
1000 des_layers.as_ref(),
1001 self.imgref.imgref.transport,
1002 )
1003 .await?;
1004 let repo = self.repo.clone();
1005 let target_ref = commit_layer.ostree_ref.clone();
1006 let import_task =
1007 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
1008 let txn = repo.auto_transaction(Some(cancellable))?;
1009 let mut importer = crate::tar::Importer::new_for_commit(&repo, remote);
1010 let blob = tokio_util::io::SyncIoBridge::new(blob);
1011 let mut blob = Decompressor::new(&media_type, blob)?;
1012 let mut archive = tar::Archive::new(&mut blob);
1013 importer.import_commit(&mut archive, Some(cancellable))?;
1014 let (commit, verify_text) = importer.finish_import_commit();
1015 if write_refs {
1016 repo.transaction_set_ref(None, &target_ref, Some(commit.as_str()));
1017 tracing::debug!("Wrote {} => {}", target_ref, commit);
1018 }
1019 repo.mark_commit_partial(&commit, false)?;
1020 txn.commit(Some(cancellable))?;
1021 blob.finish()?;
1022 Ok::<_, anyhow::Error>((commit, verify_text))
1023 });
1024 let (commit, verify_text) =
1025 super::unencapsulate::join_fetch(import_task, driver).await?;
1026 commit_layer.commit = Some(commit);
1027 import.verify_text = verify_text;
1028 if let Some(p) = self.layer_progress.as_ref() {
1029 p.send(ImportProgress::OstreeChunkCompleted(
1030 commit_layer.layer.clone(),
1031 ))
1032 .await?;
1033 }
1034 };
1035 Ok(())
1036 }
1037
1038 pub async fn unencapsulate(mut self) -> Result<Import> {
1043 let mut prep = match self.prepare_internal(false).await? {
1044 PrepareResult::AlreadyPresent(_) => {
1045 panic!("Should not have image present for unencapsulation")
1046 }
1047 PrepareResult::Ready(r) => r,
1048 };
1049 if !prep.layers.is_empty() {
1050 anyhow::bail!("Image has {} non-ostree layers", prep.layers.len());
1051 }
1052 let deprecated_warning = prep.deprecated_warning().map(ToOwned::to_owned);
1053 self.unencapsulate_base(&mut prep, true, false).await?;
1054 self.proxy.close_image(&prep.proxy_img).await?;
1057 let ostree_commit = prep.ostree_commit_layer.unwrap().commit.unwrap();
1059 let image_digest = prep.manifest_digest;
1060 Ok(Import {
1061 ostree_commit,
1062 image_digest,
1063 deprecated_warning,
1064 })
1065 }
1066
1067 fn write_merge_commit_impl(
1070 repo: &ostree::Repo,
1071 base_commit: Option<&str>,
1072 layer_commits: &[String],
1073 have_derived_layers: bool,
1074 metadata: glib::Variant,
1075 timestamp: u64,
1076 ostree_ref: &str,
1077 no_imgref: bool,
1078 disable_gc: bool,
1079 cancellable: Option<&gio::Cancellable>,
1080 ) -> Result<Box<LayeredImageState>> {
1081 use rustix::fd::AsRawFd;
1082
1083 let txn = repo.auto_transaction(cancellable)?;
1084
1085 let devino = ostree::RepoDevInoCache::new();
1086 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
1087 let repo_tmp = repodir.open_dir("tmp")?;
1088 let td = cap_std_ext::cap_tempfile::TempDir::new_in(&repo_tmp)?;
1089
1090 let rootpath = "root";
1091 let checkout_mode = if repo.mode() == ostree::RepoMode::Bare {
1092 ostree::RepoCheckoutMode::None
1093 } else {
1094 ostree::RepoCheckoutMode::User
1095 };
1096 let mut checkout_opts = ostree::RepoCheckoutAtOptions {
1097 mode: checkout_mode,
1098 overwrite_mode: ostree::RepoCheckoutOverwriteMode::UnionFiles,
1099 devino_to_csum_cache: Some(devino.clone()),
1100 no_copy_fallback: true,
1101 force_copy_zerosized: true,
1102 process_whiteouts: false,
1103 ..Default::default()
1104 };
1105 if let Some(base) = base_commit.as_ref() {
1106 repo.checkout_at(
1107 Some(&checkout_opts),
1108 (*td).as_raw_fd(),
1109 rootpath,
1110 &base,
1111 cancellable,
1112 )
1113 .context("Checking out base commit")?;
1114 }
1115
1116 checkout_opts.process_whiteouts = true;
1118 for commit in layer_commits {
1119 tracing::debug!("Unpacking {commit}");
1120 repo.checkout_at(
1121 Some(&checkout_opts),
1122 (*td).as_raw_fd(),
1123 rootpath,
1124 &commit,
1125 cancellable,
1126 )
1127 .with_context(|| format!("Checking out layer {commit}"))?;
1128 }
1129
1130 let root_dir = td.open_dir(rootpath)?;
1131
1132 let modifier =
1133 ostree::RepoCommitModifier::new(ostree::RepoCommitModifierFlags::empty(), None);
1134 modifier.set_devino_cache(&devino);
1135 if have_derived_layers {
1139 let sepolicy = ostree::SePolicy::new_at(root_dir.as_raw_fd(), cancellable)?;
1140 tracing::debug!("labeling from merged tree");
1141 modifier.set_sepolicy(Some(&sepolicy));
1142 } else if let Some(base) = base_commit.as_ref() {
1143 tracing::debug!("labeling from base tree");
1144 modifier.set_sepolicy_from_commit(repo, &base, cancellable)?;
1146 } else {
1147 panic!("Unexpected state: no derived layers and no base")
1148 }
1149
1150 cleanup_root(&root_dir)?;
1151
1152 let mt = ostree::MutableTree::new();
1153 repo.write_dfd_to_mtree(
1154 (*td).as_raw_fd(),
1155 rootpath,
1156 &mt,
1157 Some(&modifier),
1158 cancellable,
1159 )
1160 .context("Writing merged filesystem to mtree")?;
1161
1162 let merged_root = repo
1163 .write_mtree(&mt, cancellable)
1164 .context("Writing mtree")?;
1165 let merged_root = merged_root.downcast::<ostree::RepoFile>().unwrap();
1166 let parent = base_commit.as_deref();
1169 let merged_commit = repo
1170 .write_commit_with_time(
1171 parent,
1172 None,
1173 None,
1174 Some(&metadata),
1175 &merged_root,
1176 timestamp,
1177 cancellable,
1178 )
1179 .context("Writing commit")?;
1180 if !no_imgref {
1181 repo.transaction_set_ref(None, ostree_ref, Some(merged_commit.as_str()));
1182 }
1183 txn.commit(cancellable)?;
1184
1185 if !disable_gc {
1186 let n: u32 = gc_image_layers_impl(repo, cancellable)?;
1187 tracing::debug!("pruned {n} layers");
1188 }
1189
1190 let state = query_image_commit(repo, &merged_commit)?;
1193 Ok(state)
1194 }
1195
1196 #[context("Importing")]
1200 pub async fn import(
1201 mut self,
1202 mut import: Box<PreparedImport>,
1203 ) -> Result<Box<LayeredImageState>> {
1204 if let Some(status) = import.format_layer_status() {
1205 system_repo_journal_print(&self.repo, libsystemd::logging::Priority::Info, &status);
1206 }
1207 self.unencapsulate_base(&mut import, false, true).await?;
1210 let des_layers = self.proxy.get_layer_info(&import.proxy_img).await?;
1211 let proxy = self.proxy;
1212 let target_imgref = self.target_imgref.as_ref().unwrap_or(&self.imgref);
1213 let base_commit = import
1214 .ostree_commit_layer
1215 .as_ref()
1216 .map(|c| c.commit.clone().unwrap());
1217
1218 let root_is_transient = if let Some(base) = base_commit.as_ref() {
1219 let rootf = self.repo.read_commit(&base, gio::Cancellable::NONE)?.0;
1220 let rootf = rootf.downcast_ref::<ostree::RepoFile>().unwrap();
1221 crate::ostree_prepareroot::overlayfs_root_enabled(rootf)?
1222 } else {
1223 true
1225 };
1226 tracing::debug!("Base rootfs is transient: {root_is_transient}");
1227
1228 let ostree_ref = ref_for_image(&target_imgref.imgref)?;
1229
1230 let mut layer_commits = Vec::new();
1231 let mut layer_filtered_content: Option<MetaFilteredData> = None;
1232 let have_derived_layers = !import.layers.is_empty();
1233 tracing::debug!("Processing layers: {}", import.layers.len());
1234 for layer in import.layers {
1235 if let Some(c) = layer.commit {
1236 tracing::debug!("Reusing fetched commit {}", c);
1237 layer_commits.push(c.to_string());
1238 } else {
1239 if let Some(p) = self.layer_progress.as_ref() {
1240 p.send(ImportProgress::DerivedLayerStarted(layer.layer.clone()))
1241 .await?;
1242 }
1243 let (blob, driver, media_type) = super::unencapsulate::fetch_layer(
1244 &proxy,
1245 &import.proxy_img,
1246 &import.manifest,
1247 &layer.layer,
1248 self.layer_byte_progress.as_ref(),
1249 des_layers.as_ref(),
1250 self.imgref.imgref.transport,
1251 )
1252 .await?;
1253 let opts = crate::tar::WriteTarOptions {
1256 base: base_commit.clone(),
1257 selinux: true,
1258 allow_nonusr: root_is_transient,
1259 retain_var: self.ostree_v2024_3,
1260 };
1261 let r = crate::tar::write_tar(
1262 &self.repo,
1263 blob,
1264 media_type,
1265 layer.ostree_ref.as_str(),
1266 Some(opts),
1267 );
1268 let r = super::unencapsulate::join_fetch(r, driver)
1269 .await
1270 .with_context(|| format!("Parsing layer blob {}", layer.layer.digest()))?;
1271 tracing::debug!("Imported layer: {}", r.commit.as_str());
1272 layer_commits.push(r.commit);
1273 let filtered_owned = HashMap::from_iter(r.filtered.clone());
1274 if let Some((filtered, n_rest)) = bootc_utils::collect_until(
1275 r.filtered.iter(),
1276 const { NonZeroUsize::new(5).unwrap() },
1277 ) {
1278 let mut msg = String::new();
1279 for (path, n) in filtered {
1280 write!(msg, "{path}: {n} ").unwrap();
1281 }
1282 if n_rest > 0 {
1283 write!(msg, "...and {n_rest} more").unwrap();
1284 }
1285 tracing::debug!("Found filtered toplevels: {msg}");
1286 layer_filtered_content
1287 .get_or_insert_default()
1288 .insert(layer.layer.digest().to_string(), filtered_owned);
1289 } else {
1290 tracing::debug!("No filtered content");
1291 }
1292 if let Some(p) = self.layer_progress.as_ref() {
1293 p.send(ImportProgress::DerivedLayerCompleted(layer.layer.clone()))
1294 .await?;
1295 }
1296 }
1297 }
1298
1299 proxy.close_image(&import.proxy_img).await?;
1302
1303 proxy.finalize().await?;
1305 tracing::debug!("finalized proxy");
1306
1307 let _ = self.layer_byte_progress.take();
1309 let _ = self.layer_progress.take();
1310
1311 let mut metadata = BTreeMap::new();
1312 metadata.insert(
1313 META_MANIFEST_DIGEST,
1314 import.manifest_digest.to_string().to_variant(),
1315 );
1316 metadata.insert(
1317 META_MANIFEST,
1318 import.manifest.to_canon_json_string()?.to_variant(),
1319 );
1320 metadata.insert(
1321 META_CONFIG,
1322 import.config.to_canon_json_string()?.to_variant(),
1323 );
1324 metadata.insert(
1325 "ostree.importer.version",
1326 env!("CARGO_PKG_VERSION").to_variant(),
1327 );
1328 let metadata = metadata.to_variant();
1329
1330 let timestamp = timestamp_of_manifest_or_config(&import.manifest, &import.config)
1331 .unwrap_or_else(|| chrono::offset::Utc::now().timestamp() as u64);
1332 let repo = self.repo;
1334 let mut state = crate::tokio_util::spawn_blocking_cancellable_flatten(
1335 move |cancellable| -> Result<Box<LayeredImageState>> {
1336 Self::write_merge_commit_impl(
1337 &repo,
1338 base_commit.as_deref(),
1339 &layer_commits,
1340 have_derived_layers,
1341 metadata,
1342 timestamp,
1343 &ostree_ref,
1344 self.no_imgref,
1345 self.disable_gc,
1346 Some(cancellable),
1347 )
1348 },
1349 )
1350 .await?;
1351 state.verify_text = import.verify_text;
1353 state.filtered_files = layer_filtered_content;
1354 Ok(state)
1355 }
1356}
1357
1358pub fn list_images(repo: &ostree::Repo) -> Result<Vec<String>> {
1360 let cancellable = gio::Cancellable::NONE;
1361 let refs = repo.list_refs_ext(
1362 Some(IMAGE_PREFIX),
1363 ostree::RepoListRefsExtFlags::empty(),
1364 cancellable,
1365 )?;
1366 refs.keys()
1367 .map(|imgname| refescape::unprefix_unescape_ref(IMAGE_PREFIX, imgname))
1368 .collect()
1369}
1370
1371fn try_query_image(
1374 repo: &ostree::Repo,
1375 imgref: &ImageReference,
1376) -> Result<Option<Box<LayeredImageState>>> {
1377 let ostree_ref = &ref_for_image(imgref)?;
1378 if let Some(merge_rev) = repo.resolve_rev(ostree_ref, true)? {
1379 match query_image_commit(repo, merge_rev.as_str()) {
1380 Ok(r) => Ok(Some(r)),
1381 Err(e) => {
1382 eprintln!("error: failed to query image commit: {e}");
1383 Ok(None)
1384 }
1385 }
1386 } else {
1387 Ok(None)
1388 }
1389}
1390
1391#[context("Querying image {imgref}")]
1393pub fn query_image(
1394 repo: &ostree::Repo,
1395 imgref: &ImageReference,
1396) -> Result<Option<Box<LayeredImageState>>> {
1397 let ostree_ref = &ref_for_image(imgref)?;
1398 let merge_rev = repo.resolve_rev(ostree_ref, true)?;
1399 merge_rev
1400 .map(|r| query_image_commit(repo, r.as_str()))
1401 .transpose()
1402}
1403
1404fn parse_cached_update(meta: &glib::VariantDict) -> Result<Option<CachedImageUpdate>> {
1406 let manifest_digest =
1408 if let Some(d) = meta.lookup::<String>(ImageImporter::CACHED_KEY_MANIFEST_DIGEST)? {
1409 d
1410 } else {
1411 return Ok(None);
1414 };
1415 let manifest_digest = Digest::from_str(&manifest_digest)?;
1416 let manifest = meta.lookup_value(ImageImporter::CACHED_KEY_MANIFEST, None);
1419 let manifest: oci_image::ImageManifest = manifest
1420 .as_ref()
1421 .and_then(|v| v.str())
1422 .map(serde_json::from_str)
1423 .transpose()?
1424 .ok_or_else(|| {
1425 anyhow!(
1426 "Expected cached manifest {}",
1427 ImageImporter::CACHED_KEY_MANIFEST
1428 )
1429 })?;
1430 let config = meta.lookup_value(ImageImporter::CACHED_KEY_CONFIG, None);
1431 let config: oci_image::ImageConfiguration = config
1432 .as_ref()
1433 .and_then(|v| v.str())
1434 .map(serde_json::from_str)
1435 .transpose()?
1436 .ok_or_else(|| {
1437 anyhow!(
1438 "Expected cached manifest {}",
1439 ImageImporter::CACHED_KEY_CONFIG
1440 )
1441 })?;
1442 Ok(Some(CachedImageUpdate {
1443 manifest,
1444 config,
1445 manifest_digest,
1446 }))
1447}
1448
1449#[context("Clearing cached update {imgref}")]
1451pub fn clear_cached_update(repo: &ostree::Repo, imgref: &ImageReference) -> Result<()> {
1452 let cancellable = gio::Cancellable::NONE;
1453 let ostree_ref = ref_for_image(imgref)?;
1454 let rev = repo.require_rev(&ostree_ref)?;
1455 let Some(commitmeta) = repo.read_commit_detached_metadata(&rev, cancellable)? else {
1456 return Ok(());
1457 };
1458
1459 let mut commitmeta: BTreeMap<String, glib::Variant> =
1461 BTreeMap::from_variant(&commitmeta).unwrap();
1462 let mut changed = false;
1463 for key in [
1464 ImageImporter::CACHED_KEY_CONFIG,
1465 ImageImporter::CACHED_KEY_MANIFEST,
1466 ImageImporter::CACHED_KEY_MANIFEST_DIGEST,
1467 ] {
1468 if commitmeta.remove(key).is_some() {
1469 changed = true;
1470 }
1471 }
1472 if !changed {
1473 return Ok(());
1474 }
1475 let commitmeta = glib::Variant::from(commitmeta);
1476 repo.write_commit_detached_metadata(&rev, Some(&commitmeta), cancellable)?;
1477 Ok(())
1478}
1479
1480pub fn query_image_commit(repo: &ostree::Repo, commit: &str) -> Result<Box<LayeredImageState>> {
1483 let merge_commit = commit.to_string();
1484 let merge_commit_obj = repo.load_commit(commit)?.0;
1485 let commit_meta = &merge_commit_obj.child_value(0);
1486 let commit_meta = &ostree::glib::VariantDict::new(Some(commit_meta));
1487 let (manifest, manifest_digest) = manifest_data_from_commitmeta(commit_meta)?;
1488 let configuration = image_config_from_commitmeta(commit_meta)?;
1489 let mut layers = manifest.layers().iter().cloned();
1490 let base_layer = layers.next().ok_or_else(|| anyhow!("No layers found"))?;
1492 let base_layer = query_layer(repo, base_layer)?;
1493 let ostree_ref = base_layer.ostree_ref.as_str();
1494 let base_commit = base_layer
1495 .commit
1496 .ok_or_else(|| anyhow!("Missing base image ref {ostree_ref}"))?;
1497
1498 let detached_commitmeta =
1499 repo.read_commit_detached_metadata(&merge_commit, gio::Cancellable::NONE)?;
1500 let detached_commitmeta = detached_commitmeta
1501 .as_ref()
1502 .map(|v| glib::VariantDict::new(Some(v)));
1503 let cached_update = detached_commitmeta
1504 .as_ref()
1505 .map(parse_cached_update)
1506 .transpose()?
1507 .flatten();
1508 let state = Box::new(LayeredImageState {
1509 base_commit,
1510 merge_commit,
1511 manifest_digest,
1512 manifest,
1513 configuration,
1514 cached_update,
1515 verify_text: None,
1517 filtered_files: None,
1518 });
1519 tracing::debug!("Wrote merge commit {}", state.merge_commit);
1520 Ok(state)
1521}
1522
1523fn manifest_for_image(repo: &ostree::Repo, imgref: &ImageReference) -> Result<ImageManifest> {
1524 let ostree_ref = ref_for_image(imgref)?;
1525 let rev = repo.require_rev(&ostree_ref)?;
1526 let (commit_obj, _) = repo.load_commit(rev.as_str())?;
1527 let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
1528 Ok(manifest_data_from_commitmeta(commit_meta)?.0)
1529}
1530
1531#[context("Copying image")]
1534pub async fn copy(
1535 src_repo: &ostree::Repo,
1536 src_imgref: &ImageReference,
1537 dest_repo: &ostree::Repo,
1538 dest_imgref: &ImageReference,
1539) -> Result<()> {
1540 let src_ostree_ref = ref_for_image(src_imgref)?;
1541 let src_commit = src_repo.require_rev(&src_ostree_ref)?;
1542 let manifest = manifest_for_image(src_repo, src_imgref)?;
1543 let layer_refs = manifest
1545 .layers()
1546 .iter()
1547 .map(ref_for_layer)
1548 .chain(std::iter::once(Ok(src_commit.to_string())));
1549 for ostree_ref in layer_refs {
1550 let ostree_ref = ostree_ref?;
1551 let src_repo = src_repo.clone();
1552 let dest_repo = dest_repo.clone();
1553 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| -> Result<_> {
1554 let cancellable = Some(cancellable);
1555 let srcfd = &format!("file:///proc/self/fd/{}", src_repo.dfd());
1556 let flags = ostree::RepoPullFlags::MIRROR;
1557 let opts = glib::VariantDict::new(None);
1558 let refs = [ostree_ref.as_str()];
1559 opts.insert("disable-verify-bindings", true);
1561 opts.insert("refs", &refs[..]);
1562 opts.insert("flags", flags.bits() as i32);
1563 let options = opts.to_variant();
1564 dest_repo.pull_with_options(srcfd, &options, None, cancellable)?;
1565 Ok(())
1566 })
1567 .await?;
1568 }
1569
1570 let dest_ostree_ref = ref_for_image(dest_imgref)?;
1571 dest_repo.set_ref_immediate(
1572 None,
1573 &dest_ostree_ref,
1574 Some(&src_commit),
1575 gio::Cancellable::NONE,
1576 )?;
1577
1578 Ok(())
1579}
1580
1581#[derive(Clone, Debug, Default)]
1583#[non_exhaustive]
1584pub struct ExportToOCIOpts {
1585 pub skip_compression: bool,
1587 pub authfile: Option<std::path::PathBuf>,
1589 pub progress_to_stdout: bool,
1591}
1592
1593fn chunking_from_layer_committed(
1598 repo: &ostree::Repo,
1599 l: &Descriptor,
1600 chunking: &mut chunking::Chunking,
1601) -> Result<()> {
1602 let mut chunk = Chunk::default();
1603 let layer_ref = &ref_for_layer(l)?;
1604 let root = repo.read_commit(layer_ref, gio::Cancellable::NONE)?.0;
1605 let e = root.enumerate_children(
1606 "standard::name,standard::size",
1607 gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS,
1608 gio::Cancellable::NONE,
1609 )?;
1610 for child in e.clone() {
1611 let child = &child?;
1612 let name = child.name();
1614 let name = Utf8Path::from_path(&name).unwrap();
1616 ostree::validate_checksum_string(name.as_str())?;
1617 chunking.remainder.move_obj(&mut chunk, name.as_str());
1618 }
1619 chunking.chunks.push(chunk);
1620 Ok(())
1621}
1622
1623#[context("Copying image")]
1625pub(crate) fn export_to_oci(
1626 repo: &ostree::Repo,
1627 imgref: &ImageReference,
1628 dest_oci: &Dir,
1629 tag: Option<&str>,
1630 opts: ExportToOCIOpts,
1631) -> Result<Descriptor> {
1632 let srcinfo = query_image(repo, imgref)?.ok_or_else(|| anyhow!("No such image"))?;
1633 let (commit_layer, component_layers, remaining_layers) =
1634 parse_manifest_layout(&srcinfo.manifest, &srcinfo.configuration)?;
1635
1636 let mut new_manifest = srcinfo.manifest.clone();
1640 new_manifest.layers_mut().clear();
1641 let mut new_config = srcinfo.configuration.clone();
1642 if let Some(history) = new_config.history_mut() {
1643 history.clear();
1644 }
1645 new_config.rootfs_mut().diff_ids_mut().clear();
1646
1647 let opts = ExportOpts {
1648 skip_compression: opts.skip_compression,
1649 authfile: opts.authfile,
1650 ..Default::default()
1651 };
1652
1653 let mut labels = HashMap::new();
1654
1655 let mut dest_oci = ocidir::OciDir::ensure(dest_oci.try_clone()?)?;
1656
1657 let commit_chunk_ref = commit_layer
1658 .as_ref()
1659 .map(|l| ref_for_layer(l))
1660 .transpose()?;
1661 let commit_chunk_rev = commit_chunk_ref
1662 .as_ref()
1663 .map(|r| repo.require_rev(&r))
1664 .transpose()?;
1665 if let Some(commit_chunk_rev) = commit_chunk_rev {
1666 let mut chunking = chunking::Chunking::new(repo, &commit_chunk_rev)?;
1667 for layer in component_layers {
1668 chunking_from_layer_committed(repo, layer, &mut chunking)?;
1669 }
1670
1671 export_chunked(
1674 repo,
1675 &srcinfo.base_commit,
1676 &mut dest_oci,
1677 &mut new_manifest,
1678 &mut new_config,
1679 &mut labels,
1680 chunking,
1681 &opts,
1682 "",
1683 )?;
1684 }
1685
1686 let compression = opts.skip_compression.then_some(Compression::none());
1688 for (i, layer) in remaining_layers.iter().enumerate() {
1689 let layer_ref = &ref_for_layer(layer)?;
1690 let mut target_blob = dest_oci.create_gzip_layer(compression)?;
1691 let export_opts = crate::tar::ExportOptions { raw: true };
1693 crate::tar::export_commit(
1694 repo,
1695 layer_ref.as_str(),
1696 &mut target_blob,
1697 Some(export_opts),
1698 )?;
1699 let layer = target_blob.complete()?;
1700 let previous_annotations = srcinfo
1701 .manifest
1702 .layers()
1703 .get(i)
1704 .and_then(|l| l.annotations().as_ref())
1705 .cloned();
1706 let history = srcinfo.configuration.history().as_ref();
1707 let history_entry = history.and_then(|v| v.get(i));
1708 let previous_description = history_entry
1709 .clone()
1710 .and_then(|h| h.comment().as_deref())
1711 .unwrap_or_default();
1712
1713 let previous_created = history_entry
1714 .and_then(|h| h.created().as_deref())
1715 .and_then(bootc_utils::try_deserialize_timestamp)
1716 .unwrap_or_default();
1717
1718 dest_oci.push_layer_full(
1719 &mut new_manifest,
1720 &mut new_config,
1721 layer,
1722 previous_annotations,
1723 previous_description,
1724 previous_created,
1725 )
1726 }
1727
1728 let new_config = dest_oci.write_config(new_config)?;
1729 new_manifest.set_config(new_config);
1730
1731 Ok(dest_oci.insert_manifest(new_manifest, tag, oci_image::Platform::default())?)
1732}
1733
1734#[context("Export")]
1737pub async fn export(
1738 repo: &ostree::Repo,
1739 src_imgref: &ImageReference,
1740 dest_imgref: &ImageReference,
1741 opts: Option<ExportToOCIOpts>,
1742) -> Result<oci_image::Digest> {
1743 let opts = opts.unwrap_or_default();
1744 let target_oci = dest_imgref.transport == Transport::OciDir;
1745 let tempdir = if !target_oci {
1746 let vartmp = cap_std::fs::Dir::open_ambient_dir("/var/tmp", cap_std::ambient_authority())?;
1747 let td = cap_std_ext::cap_tempfile::TempDir::new_in(&vartmp)?;
1748 let opts = ExportToOCIOpts {
1750 skip_compression: true,
1751 progress_to_stdout: opts.progress_to_stdout,
1752 ..Default::default()
1753 };
1754 export_to_oci(repo, src_imgref, &td, None, opts)?;
1755 td
1756 } else {
1757 let (path, tag) = parse_oci_path_and_tag(dest_imgref.name.as_str());
1758 tracing::debug!("using OCI path={path} tag={tag:?}");
1759 let path = Dir::open_ambient_dir(path, cap_std::ambient_authority())
1760 .with_context(|| format!("Opening {path}"))?;
1761 let descriptor = export_to_oci(repo, src_imgref, &path, tag, opts)?;
1762 return Ok(descriptor.digest().clone());
1763 };
1764 let target_fd = 3i32;
1766 let tempoci = ImageReference {
1767 transport: Transport::OciDir,
1768 name: format!("/proc/self/fd/{target_fd}"),
1769 };
1770 let authfile = opts.authfile.as_deref();
1771 skopeo::copy(
1772 &tempoci,
1773 dest_imgref,
1774 authfile,
1775 Some((std::sync::Arc::new(tempdir.try_clone()?.into()), target_fd)),
1776 opts.progress_to_stdout,
1777 )
1778 .await
1779}
1780
1781#[context("Listing deployment manifests")]
1784fn list_container_deployment_manifests(
1785 repo: &ostree::Repo,
1786 cancellable: Option<&gio::Cancellable>,
1787) -> Result<Vec<ImageManifest>> {
1788 let commits = OSTREE_BASE_DEPLOYMENT_REFS
1791 .iter()
1792 .chain(RPMOSTREE_BASE_REFS)
1793 .chain(std::iter::once(&BASE_IMAGE_PREFIX))
1794 .try_fold(
1795 std::collections::HashSet::new(),
1796 |mut acc, &p| -> Result<_> {
1797 let refs = repo.list_refs_ext(
1798 Some(p),
1799 ostree::RepoListRefsExtFlags::empty(),
1800 cancellable,
1801 )?;
1802 for (_, v) in refs {
1803 acc.insert(v);
1804 }
1805 Ok(acc)
1806 },
1807 )?;
1808 let mut r = Vec::new();
1810 for commit in commits {
1811 let commit_obj = repo.load_commit(&commit)?.0;
1812 let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
1813 if commit_meta
1814 .lookup::<String>(META_MANIFEST_DIGEST)?
1815 .is_some()
1816 {
1817 tracing::trace!("Commit {commit} is a container image");
1818 let manifest = manifest_data_from_commitmeta(commit_meta)?.0;
1819 r.push(manifest);
1820 }
1821 }
1822 Ok(r)
1823}
1824
1825pub fn gc_image_layers(repo: &ostree::Repo) -> Result<u32> {
1831 gc_image_layers_impl(repo, gio::Cancellable::NONE)
1832}
1833
1834#[context("Pruning image layers")]
1835fn gc_image_layers_impl(
1836 repo: &ostree::Repo,
1837 cancellable: Option<&gio::Cancellable>,
1838) -> Result<u32> {
1839 let all_images = list_images(repo)?;
1840 let deployment_commits = list_container_deployment_manifests(repo, cancellable)?;
1841 let all_manifests = all_images
1842 .into_iter()
1843 .map(|img| {
1844 ImageReference::try_from(img.as_str()).and_then(|ir| manifest_for_image(repo, &ir))
1845 })
1846 .chain(deployment_commits.into_iter().map(Ok))
1847 .collect::<Result<Vec<_>>>()?;
1848 tracing::debug!("Images found: {}", all_manifests.len());
1849 let mut referenced_layers = BTreeSet::new();
1850 for m in all_manifests.iter() {
1851 for layer in m.layers() {
1852 referenced_layers.insert(layer.digest().to_string());
1853 }
1854 }
1855 tracing::debug!("Referenced layers: {}", referenced_layers.len());
1856 let found_layers = repo
1857 .list_refs_ext(
1858 Some(LAYER_PREFIX),
1859 ostree::RepoListRefsExtFlags::empty(),
1860 cancellable,
1861 )?
1862 .into_iter()
1863 .map(|v| v.0);
1864 tracing::debug!("Found layers: {}", found_layers.len());
1865 let mut pruned = 0u32;
1866 for layer_ref in found_layers {
1867 let layer_digest = refescape::unprefix_unescape_ref(LAYER_PREFIX, &layer_ref)?;
1868 if referenced_layers.remove(layer_digest.as_str()) {
1869 continue;
1870 }
1871 pruned += 1;
1872 tracing::debug!("Pruning: {}", layer_ref.as_str());
1873 repo.set_ref_immediate(None, layer_ref.as_str(), None, cancellable)?;
1874 }
1875
1876 Ok(pruned)
1877}
1878
1879#[cfg(feature = "internal-testing-api")]
1880pub fn count_layer_references(repo: &ostree::Repo) -> Result<u32> {
1882 let cancellable = gio::Cancellable::NONE;
1883 let n = repo
1884 .list_refs_ext(
1885 Some(LAYER_PREFIX),
1886 ostree::RepoListRefsExtFlags::empty(),
1887 cancellable,
1888 )?
1889 .len();
1890 Ok(n as u32)
1891}
1892
1893pub fn image_filtered_content_warning(
1895 filtered_files: &Option<MetaFilteredData>,
1896) -> Result<Option<String>> {
1897 use std::fmt::Write;
1898
1899 let r = filtered_files.as_ref().map(|v| {
1900 let mut filtered = BTreeMap::<&String, u32>::new();
1901 for paths in v.values() {
1902 for (k, v) in paths {
1903 let e = filtered.entry(k).or_default();
1904 *e += v;
1905 }
1906 }
1907 let mut buf = "Image contains non-ostree compatible file paths:".to_string();
1908 for (k, v) in filtered {
1909 write!(buf, " {k}: {v}").unwrap();
1910 }
1911 buf
1912 });
1913 Ok(r)
1914}
1915
1916#[context("Pruning {img}")]
1923pub fn remove_image(repo: &ostree::Repo, img: &ImageReference) -> Result<bool> {
1924 let ostree_ref = &ref_for_image(img)?;
1925 let found = repo.resolve_rev(ostree_ref, true)?.is_some();
1926 if found {
1929 repo.set_ref_immediate(None, ostree_ref, None, gio::Cancellable::NONE)?;
1930 }
1931 Ok(found)
1932}
1933
1934pub fn remove_images<'a>(
1941 repo: &ostree::Repo,
1942 imgs: impl IntoIterator<Item = &'a ImageReference>,
1943) -> Result<()> {
1944 let mut missing = Vec::new();
1945 for img in imgs.into_iter() {
1946 let found = remove_image(repo, img)?;
1947 if !found {
1948 missing.push(img);
1949 }
1950 }
1951 if !missing.is_empty() {
1952 let missing = missing.into_iter().fold("".to_string(), |mut a, v| {
1953 a.push_str(&v.to_string());
1954 a
1955 });
1956 return Err(anyhow::anyhow!("Missing images: {missing}"));
1957 }
1958 Ok(())
1959}
1960
1961#[derive(Debug, Default)]
1962struct CompareState {
1963 verified: BTreeSet<Utf8PathBuf>,
1964 inode_corrupted: BTreeSet<Utf8PathBuf>,
1965 unknown_corrupted: BTreeSet<Utf8PathBuf>,
1966}
1967
1968impl CompareState {
1969 fn is_ok(&self) -> bool {
1970 self.inode_corrupted.is_empty() && self.unknown_corrupted.is_empty()
1971 }
1972}
1973
1974fn compare_file_info(src: &gio::FileInfo, target: &gio::FileInfo) -> bool {
1975 if src.file_type() != target.file_type() {
1976 return false;
1977 }
1978 if src.size() != target.size() {
1979 return false;
1980 }
1981 for attr in ["unix::uid", "unix::gid", "unix::mode"] {
1982 if src.attribute_uint32(attr) != target.attribute_uint32(attr) {
1983 return false;
1984 }
1985 }
1986 true
1987}
1988
1989#[context("Querying object inode")]
1990fn inode_of_object(repo: &ostree::Repo, checksum: &str) -> Result<u64> {
1991 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
1992 let (prefix, suffix) = checksum.split_at(2);
1993 let objpath = format!("objects/{prefix}/{suffix}.file");
1994 let metadata = repodir.symlink_metadata(objpath)?;
1995 Ok(metadata.ino())
1996}
1997
1998fn compare_commit_trees(
1999 repo: &ostree::Repo,
2000 root: &Utf8Path,
2001 target: &ostree::RepoFile,
2002 expected: &ostree::RepoFile,
2003 exact: bool,
2004 colliding_inodes: &BTreeSet<u64>,
2005 state: &mut CompareState,
2006) -> Result<()> {
2007 let cancellable = gio::Cancellable::NONE;
2008 let queryattrs = "standard::name,standard::type";
2009 let queryflags = gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS;
2010 let expected_iter = expected.enumerate_children(queryattrs, queryflags, cancellable)?;
2011
2012 while let Some(expected_info) = expected_iter.next_file(cancellable)? {
2013 let expected_child = expected_iter.child(&expected_info);
2014 let name = expected_info.name();
2015 let name = name.to_str().expect("UTF-8 ostree name");
2016 let path = Utf8PathBuf::from(format!("{root}{name}"));
2017 let target_child = target.child(name);
2018 let target_info = crate::diff::query_info_optional(&target_child, queryattrs, queryflags)
2019 .context("querying optional to")?;
2020 let is_dir = matches!(expected_info.file_type(), gio::FileType::Directory);
2021 if let Some(target_info) = target_info {
2022 let to_child = target_child
2023 .downcast::<ostree::RepoFile>()
2024 .expect("downcast");
2025 to_child.ensure_resolved()?;
2026 let from_child = expected_child
2027 .downcast::<ostree::RepoFile>()
2028 .expect("downcast");
2029 from_child.ensure_resolved()?;
2030
2031 if is_dir {
2032 let from_contents_checksum = from_child.tree_get_contents_checksum();
2033 let to_contents_checksum = to_child.tree_get_contents_checksum();
2034 if from_contents_checksum != to_contents_checksum {
2035 let subpath = Utf8PathBuf::from(format!("{path}/"));
2036 compare_commit_trees(
2037 repo,
2038 &subpath,
2039 &from_child,
2040 &to_child,
2041 exact,
2042 colliding_inodes,
2043 state,
2044 )?;
2045 }
2046 } else {
2047 let from_checksum = from_child.checksum();
2048 let to_checksum = to_child.checksum();
2049 let matches = if exact {
2050 from_checksum == to_checksum
2051 } else {
2052 compare_file_info(&target_info, &expected_info)
2053 };
2054 if !matches {
2055 let from_inode = inode_of_object(repo, &from_checksum)?;
2056 let to_inode = inode_of_object(repo, &to_checksum)?;
2057 if colliding_inodes.contains(&from_inode)
2058 || colliding_inodes.contains(&to_inode)
2059 {
2060 state.inode_corrupted.insert(path);
2061 } else {
2062 state.unknown_corrupted.insert(path);
2063 }
2064 } else {
2065 state.verified.insert(path);
2066 }
2067 }
2068 } else {
2069 eprintln!("Missing {path}");
2070 state.unknown_corrupted.insert(path);
2071 }
2072 }
2073 Ok(())
2074}
2075
2076#[context("Verifying container image state")]
2077pub(crate) fn verify_container_image(
2078 sysroot: &SysrootLock,
2079 imgref: &ImageReference,
2080 state: &LayeredImageState,
2081 colliding_inodes: &BTreeSet<u64>,
2082 verbose: bool,
2083) -> Result<bool> {
2084 let cancellable = gio::Cancellable::NONE;
2085 let repo = &sysroot.repo();
2086 let merge_commit = state.merge_commit.as_str();
2087 let merge_commit_root = repo.read_commit(merge_commit, gio::Cancellable::NONE)?.0;
2088 let merge_commit_root = merge_commit_root
2089 .downcast::<ostree::RepoFile>()
2090 .expect("downcast");
2091 merge_commit_root.ensure_resolved()?;
2092
2093 let (commit_layer, _component_layers, remaining_layers) =
2094 parse_manifest_layout(&state.manifest, &state.configuration)?;
2095
2096 let mut comparison_state = CompareState::default();
2097
2098 let query = |l: &Descriptor| query_layer(repo, l.clone());
2099
2100 let base_tree = repo
2101 .read_commit(&state.base_commit, cancellable)?
2102 .0
2103 .downcast::<ostree::RepoFile>()
2104 .expect("downcast");
2105 if let Some(commit_layer) = commit_layer {
2106 println!(
2107 "Verifying with base ostree layer {}",
2108 ref_for_layer(commit_layer)?
2109 );
2110 }
2111 compare_commit_trees(
2112 repo,
2113 "/".into(),
2114 &merge_commit_root,
2115 &base_tree,
2116 true,
2117 colliding_inodes,
2118 &mut comparison_state,
2119 )?;
2120
2121 let remaining_layers = remaining_layers
2122 .into_iter()
2123 .map(query)
2124 .collect::<Result<Vec<_>>>()?;
2125
2126 println!("Image has {} derived layers", remaining_layers.len());
2127
2128 for layer in remaining_layers.iter().rev() {
2129 let layer_ref = layer.ostree_ref.as_str();
2130 let layer_commit = layer
2131 .commit
2132 .as_deref()
2133 .ok_or_else(|| anyhow!("Missing layer {layer_ref}"))?;
2134 let layer_tree = repo
2135 .read_commit(layer_commit, cancellable)?
2136 .0
2137 .downcast::<ostree::RepoFile>()
2138 .expect("downcast");
2139 compare_commit_trees(
2140 repo,
2141 "/".into(),
2142 &merge_commit_root,
2143 &layer_tree,
2144 false,
2145 colliding_inodes,
2146 &mut comparison_state,
2147 )?;
2148 }
2149
2150 let n_verified = comparison_state.verified.len();
2151 if comparison_state.is_ok() {
2152 println!("OK image {imgref} (verified={n_verified})");
2153 println!();
2154 } else {
2155 let n_inode = comparison_state.inode_corrupted.len();
2156 let n_other = comparison_state.unknown_corrupted.len();
2157 eprintln!("warning: Found corrupted merge commit");
2158 eprintln!(" inode clashes: {n_inode}");
2159 eprintln!(" unknown: {n_other}");
2160 eprintln!(" ok: {n_verified}");
2161 if verbose {
2162 eprintln!("Mismatches:");
2163 for path in comparison_state.inode_corrupted {
2164 eprintln!(" inode: {path}");
2165 }
2166 for path in comparison_state.unknown_corrupted {
2167 eprintln!(" other: {path}");
2168 }
2169 }
2170 eprintln!();
2171 return Ok(false);
2172 }
2173
2174 Ok(true)
2175}
2176
2177#[cfg(test)]
2178mod tests {
2179 use cap_std_ext::cap_tempfile;
2180 use oci_image::{DescriptorBuilder, MediaType, Sha256Digest};
2181
2182 use super::*;
2183
2184 #[test]
2185 fn test_ref_for_descriptor() {
2186 let d = DescriptorBuilder::default()
2187 .size(42u64)
2188 .media_type(MediaType::ImageManifest)
2189 .digest(
2190 Sha256Digest::from_str(
2191 "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
2192 )
2193 .unwrap(),
2194 )
2195 .build()
2196 .unwrap();
2197 assert_eq!(
2198 ref_for_layer(&d).unwrap(),
2199 "ostree/container/blob/sha256_3A_2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
2200 );
2201 }
2202
2203 #[test]
2204 fn test_cleanup_root() -> Result<()> {
2205 let td = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2206 let usretc = "usr/etc";
2207 cleanup_root(&td).unwrap();
2208 td.create_dir_all(usretc)?;
2209 let usretc = &td.open_dir(usretc)?;
2210 usretc.write("hostname", b"hostname")?;
2211 cleanup_root(&td).unwrap();
2212 assert!(usretc.try_exists("hostname")?);
2213 usretc.write("hostname", b"")?;
2214 cleanup_root(&td).unwrap();
2215 assert!(!td.try_exists("hostname")?);
2216
2217 usretc.symlink_contents("../run/systemd/stub-resolv.conf", "resolv.conf")?;
2218 cleanup_root(&td).unwrap();
2219 assert!(usretc.symlink_metadata("resolv.conf")?.is_symlink());
2220 usretc.remove_file("resolv.conf")?;
2221 usretc.write("resolv.conf", b"")?;
2222 cleanup_root(&td).unwrap();
2223 assert!(!usretc.try_exists("resolv.conf")?);
2224
2225 Ok(())
2226 }
2227}