1use super::*;
9use crate::chunking::{self, Chunk};
10use crate::generic_decompress::Decompressor;
11use crate::logging::system_repo_journal_print;
12use crate::refescape;
13use crate::sysroot::SysrootLock;
14use anyhow::{anyhow, Context};
15use bootc_utils::ResultExt;
16use camino::{Utf8Path, Utf8PathBuf};
17use canon_json::CanonJsonSerialize;
18use cap_std_ext::cap_std;
19use cap_std_ext::cap_std::fs::{Dir, MetadataExt};
20
21use cap_std_ext::dirext::CapStdExtDirExt;
22use containers_image_proxy::{ImageProxy, OpenedImage};
23use flate2::Compression;
24use fn_error_context::context;
25use futures_util::TryFutureExt;
26use glib::prelude::*;
27use oci_spec::image::{
28 self as oci_image, Arch, Descriptor, Digest, History, ImageConfiguration, ImageManifest,
29};
30use ocidir::oci_spec::distribution::Reference;
31use ostree::prelude::{Cast, FileEnumeratorExt, FileExt, ToVariant};
32use ostree::{gio, glib};
33use std::collections::{BTreeMap, BTreeSet, HashMap};
34use std::fmt::Write as _;
35use std::iter::FromIterator;
36use std::num::NonZeroUsize;
37use tokio::sync::mpsc::{Receiver, Sender};
38
39pub use containers_image_proxy::ImageProxyConfig;
44
45const LAYER_PREFIX: &str = "ostree/container/blob";
47const IMAGE_PREFIX: &str = "ostree/container/image";
49pub const BASE_IMAGE_PREFIX: &str = "ostree/container/baseimage";
54
55pub(crate) const META_MANIFEST_DIGEST: &str = "ostree.manifest-digest";
57const META_MANIFEST: &str = "ostree.manifest";
59const META_CONFIG: &str = "ostree.container.image-config";
61pub type MetaFilteredData = HashMap<String, HashMap<String, u32>>;
63
64const OSTREE_BASE_DEPLOYMENT_REFS: &[&str] = &["ostree/0", "ostree/1"];
66const RPMOSTREE_BASE_REFS: &[&str] = &["rpmostree/base"];
68
69fn ref_for_blob_digest(d: &str) -> Result<String> {
71 refescape::prefix_escape_for_ref(LAYER_PREFIX, d)
72}
73
74fn ref_for_layer(l: &oci_image::Descriptor) -> Result<String> {
76 ref_for_blob_digest(&l.digest().as_ref())
77}
78
79fn ref_for_image(l: &ImageReference) -> Result<String> {
81 refescape::prefix_escape_for_ref(IMAGE_PREFIX, &l.to_string())
82}
83
84#[derive(Debug)]
86pub enum ImportProgress {
87 OstreeChunkStarted(Descriptor),
89 OstreeChunkCompleted(Descriptor),
91 DerivedLayerStarted(Descriptor),
93 DerivedLayerCompleted(Descriptor),
95}
96
97impl ImportProgress {
98 pub fn is_starting(&self) -> bool {
100 match self {
101 ImportProgress::OstreeChunkStarted(_) => true,
102 ImportProgress::OstreeChunkCompleted(_) => false,
103 ImportProgress::DerivedLayerStarted(_) => true,
104 ImportProgress::DerivedLayerCompleted(_) => false,
105 }
106 }
107}
108
109#[derive(Clone, Debug)]
111pub struct LayerProgress {
112 pub layer_index: usize,
114 pub fetched: u64,
116 pub total: u64,
118}
119
120#[derive(Debug, PartialEq, Eq)]
122pub struct LayeredImageState {
123 pub base_commit: String,
125 pub merge_commit: String,
127 pub manifest_digest: Digest,
129 pub manifest: ImageManifest,
131 pub configuration: ImageConfiguration,
133 pub cached_update: Option<CachedImageUpdate>,
135 pub verify_text: Option<String>,
139 pub filtered_files: Option<MetaFilteredData>,
141}
142
143impl LayeredImageState {
144 pub fn get_commit(&self) -> &str {
148 self.merge_commit.as_str()
149 }
150
151 pub fn version(&self) -> Option<&str> {
153 super::version_for_config(&self.configuration)
154 }
155}
156
157#[derive(Debug, PartialEq, Eq)]
159pub struct CachedImageUpdate {
160 pub manifest: ImageManifest,
162 pub config: ImageConfiguration,
164 pub manifest_digest: Digest,
166}
167
168impl CachedImageUpdate {
169 pub fn version(&self) -> Option<&str> {
171 super::version_for_config(&self.config)
172 }
173}
174
175#[derive(Debug)]
177pub struct ImageImporter {
178 repo: ostree::Repo,
179 pub(crate) proxy: ImageProxy,
180 imgref: OstreeImageReference,
181 target_imgref: Option<OstreeImageReference>,
182 no_imgref: bool, disable_gc: bool, require_bootable: bool,
186 offline: bool,
188 ostree_v2024_3: bool,
190
191 layer_progress: Option<Sender<ImportProgress>>,
192 layer_byte_progress: Option<tokio::sync::watch::Sender<Option<LayerProgress>>>,
193}
194
195#[derive(Debug)]
197pub enum PrepareResult {
198 AlreadyPresent(Box<LayeredImageState>),
200 Ready(Box<PreparedImport>),
202}
203
204#[derive(Debug)]
206pub struct ManifestLayerState {
207 pub layer: oci_image::Descriptor,
209 pub ostree_ref: String,
212 pub commit: Option<String>,
215}
216
217impl ManifestLayerState {
218 pub fn layer(&self) -> &oci_image::Descriptor {
220 &self.layer
221 }
222}
223
224#[derive(Debug)]
226pub struct PreparedImport {
227 pub manifest_digest: Digest,
229 pub manifest: oci_image::ImageManifest,
231 pub config: oci_image::ImageConfiguration,
233 pub previous_state: Option<Box<LayeredImageState>>,
235 pub previous_manifest_digest: Option<Digest>,
237 pub previous_imageid: Option<String>,
239 pub ostree_layers: Vec<ManifestLayerState>,
241 pub ostree_commit_layer: Option<ManifestLayerState>,
243 pub layers: Vec<ManifestLayerState>,
245 pub verify_text: Option<String>,
247 proxy_img: OpenedImage,
249}
250
251impl PreparedImport {
252 pub fn all_layers(&self) -> impl Iterator<Item = &ManifestLayerState> {
254 self.ostree_commit_layer
255 .iter()
256 .chain(self.ostree_layers.iter())
257 .chain(self.layers.iter())
258 }
259
260 pub fn version(&self) -> Option<&str> {
262 super::version_for_config(&self.config)
263 }
264
265 pub fn deprecated_warning(&self) -> Option<&'static str> {
267 None
268 }
269
270 pub fn layers_with_history(
273 &self,
274 ) -> impl Iterator<Item = Result<(&ManifestLayerState, &History)>> {
275 let truncated = std::iter::once_with(|| Err(anyhow::anyhow!("Truncated history")));
277 let history = self
278 .config
279 .history()
280 .iter()
281 .flatten()
282 .map(Ok)
283 .chain(truncated);
284 self.all_layers()
285 .zip(history)
286 .map(|(s, h)| h.map(|h| (s, h)))
287 }
288
289 pub fn layers_to_fetch(&self) -> impl Iterator<Item = Result<(&ManifestLayerState, &str)>> {
291 self.layers_with_history().filter_map(|r| {
292 r.map(|(l, h)| {
293 l.commit.is_none().then(|| {
294 let comment = h.created_by().as_deref().unwrap_or("");
295 (l, comment)
296 })
297 })
298 .transpose()
299 })
300 }
301
302 pub(crate) fn format_layer_status(&self) -> Option<String> {
304 let (stored, to_fetch, to_fetch_size) =
305 self.all_layers()
306 .fold((0u32, 0u32, 0u64), |(stored, to_fetch, sz), v| {
307 if v.commit.is_some() {
308 (stored + 1, to_fetch, sz)
309 } else {
310 (stored, to_fetch + 1, sz + v.layer().size())
311 }
312 });
313 (to_fetch > 0).then(|| {
314 let size = crate::glib::format_size(to_fetch_size);
315 format!("layers already present: {stored}; layers needed: {to_fetch} ({size})")
316 })
317 }
318}
319
320pub(crate) fn query_layer(
322 repo: &ostree::Repo,
323 layer: oci_image::Descriptor,
324) -> Result<ManifestLayerState> {
325 let ostree_ref = ref_for_layer(&layer)?;
326 let commit = repo.resolve_rev(&ostree_ref, true)?.map(|s| s.to_string());
327 Ok(ManifestLayerState {
328 layer,
329 ostree_ref,
330 commit,
331 })
332}
333
334#[context("Reading manifest data from commit")]
335fn manifest_data_from_commitmeta(
336 commit_meta: &glib::VariantDict,
337) -> Result<(oci_image::ImageManifest, Digest)> {
338 let digest = commit_meta
339 .lookup::<String>(META_MANIFEST_DIGEST)?
340 .ok_or_else(|| anyhow!("Missing {} metadata on merge commit", META_MANIFEST_DIGEST))?;
341 let digest = Digest::from_str(&digest)?;
342 let manifest_bytes: String = commit_meta
343 .lookup::<String>(META_MANIFEST)?
344 .ok_or_else(|| anyhow!("Failed to find {} metadata key", META_MANIFEST))?;
345 let r = serde_json::from_str(&manifest_bytes)?;
346 Ok((r, digest))
347}
348
349fn image_config_from_commitmeta(commit_meta: &glib::VariantDict) -> Result<ImageConfiguration> {
350 let config = if let Some(config) = commit_meta
351 .lookup::<String>(META_CONFIG)?
352 .filter(|v| v != "null") .map(|v| serde_json::from_str(&v).map_err(anyhow::Error::msg))
354 .transpose()?
355 {
356 config
357 } else {
358 tracing::debug!("No image configuration found");
359 Default::default()
360 };
361 Ok(config)
362}
363
364pub fn manifest_digest_from_commit(commit: &glib::Variant) -> Result<Digest> {
370 let commit_meta = &commit.child_value(0);
371 let commit_meta = &glib::VariantDict::new(Some(commit_meta));
372 Ok(manifest_data_from_commitmeta(commit_meta)?.1)
373}
374
375fn layer_from_diffid<'a>(
379 manifest: &'a ImageManifest,
380 config: &ImageConfiguration,
381 diffid: &str,
382) -> Result<&'a Descriptor> {
383 let idx = config
384 .rootfs()
385 .diff_ids()
386 .iter()
387 .position(|x| x.as_str() == diffid)
388 .ok_or_else(|| anyhow!("Missing {} {}", DIFFID_LABEL, diffid))?;
389 manifest.layers().get(idx).ok_or_else(|| {
390 anyhow!(
391 "diffid position {} exceeds layer count {}",
392 idx,
393 manifest.layers().len()
394 )
395 })
396}
397
398#[context("Parsing manifest layout")]
399pub(crate) fn parse_manifest_layout<'a>(
400 manifest: &'a ImageManifest,
401 config: &ImageConfiguration,
402) -> Result<(
403 Option<&'a Descriptor>,
404 Vec<&'a Descriptor>,
405 Vec<&'a Descriptor>,
406)> {
407 let config_labels = super::labels_of(config);
408
409 let first_layer = manifest
410 .layers()
411 .first()
412 .ok_or_else(|| anyhow!("No layers in manifest"))?;
413 let Some(target_diffid) = config_labels.and_then(|labels| labels.get(DIFFID_LABEL)) else {
414 return Ok((None, Vec::new(), manifest.layers().iter().collect()));
415 };
416
417 let target_layer = layer_from_diffid(manifest, config, target_diffid.as_str())?;
418 let mut chunk_layers = Vec::new();
419 let mut derived_layers = Vec::new();
420 let mut after_target = false;
421 let ostree_layer = first_layer;
423 for layer in manifest.layers() {
424 if layer == target_layer {
425 if after_target {
426 anyhow::bail!("Multiple entries for {}", layer.digest());
427 }
428 after_target = true;
429 if layer != ostree_layer {
430 chunk_layers.push(layer);
431 }
432 } else if !after_target {
433 if layer != ostree_layer {
434 chunk_layers.push(layer);
435 }
436 } else {
437 derived_layers.push(layer);
438 }
439 }
440
441 Ok((Some(ostree_layer), chunk_layers, derived_layers))
442}
443
444#[context("Parsing manifest layout")]
446pub(crate) fn parse_ostree_manifest_layout<'a>(
447 manifest: &'a ImageManifest,
448 config: &ImageConfiguration,
449) -> Result<(&'a Descriptor, Vec<&'a Descriptor>, Vec<&'a Descriptor>)> {
450 let (ostree_layer, component_layers, derived_layers) = parse_manifest_layout(manifest, config)?;
451 let ostree_layer = ostree_layer.ok_or_else(|| {
452 anyhow!("No {DIFFID_LABEL} label found, not an ostree encapsulated container")
453 })?;
454 Ok((ostree_layer, component_layers, derived_layers))
455}
456
457fn timestamp_of_manifest_or_config(
459 manifest: &ImageManifest,
460 config: &ImageConfiguration,
461) -> Option<u64> {
462 let timestamp = manifest
465 .annotations()
466 .as_ref()
467 .and_then(|a| a.get(oci_image::ANNOTATION_CREATED))
468 .or_else(|| config.created().as_ref());
469 timestamp
471 .map(|t| {
472 chrono::DateTime::parse_from_rfc3339(t)
473 .context("Failed to parse manifest timestamp")
474 .map(|t| t.timestamp() as u64)
475 })
476 .transpose()
477 .log_err_default()
478}
479
480fn cleanup_root(root: &Dir) -> Result<()> {
483 const RUNTIME_INJECTED: &[&str] = &["usr/etc/hostname", "usr/etc/resolv.conf"];
484 for ent in RUNTIME_INJECTED {
485 if let Some(meta) = root.symlink_metadata_optional(ent)? {
486 if meta.is_file() && meta.size() == 0 {
487 tracing::debug!("Removing {ent}");
488 root.remove_file(ent)?;
489 }
490 }
491 }
492 Ok(())
493}
494
495impl ImageImporter {
496 const CACHED_KEY_MANIFEST_DIGEST: &'static str = "ostree-ext.cached.manifest-digest";
498 const CACHED_KEY_MANIFEST: &'static str = "ostree-ext.cached.manifest";
499 const CACHED_KEY_CONFIG: &'static str = "ostree-ext.cached.config";
500
501 #[context("Creating importer")]
503 pub async fn new(
504 repo: &ostree::Repo,
505 imgref: &OstreeImageReference,
506 mut config: ImageProxyConfig,
507 ) -> Result<Self> {
508 if imgref.imgref.transport == Transport::ContainerStorage {
509 merge_default_container_proxy_opts_with_isolation(&mut config, None)?;
511 } else {
512 merge_default_container_proxy_opts(&mut config)?;
514 }
515 let proxy = ImageProxy::new_with_config(config).await?;
516
517 system_repo_journal_print(
518 repo,
519 libsystemd::logging::Priority::Info,
520 &format!("Fetching {imgref}"),
521 );
522
523 let repo = repo.clone();
524 Ok(ImageImporter {
525 repo,
526 proxy,
527 target_imgref: None,
528 no_imgref: false,
529 ostree_v2024_3: ostree::check_version(2024, 3),
530 disable_gc: false,
531 require_bootable: false,
532 offline: false,
533 imgref: imgref.clone(),
534 layer_progress: None,
535 layer_byte_progress: None,
536 })
537 }
538
539 pub fn set_target(&mut self, target: &OstreeImageReference) {
541 self.target_imgref = Some(target.clone())
542 }
543
544 pub fn set_no_imgref(&mut self) {
548 self.no_imgref = true;
549 }
550
551 pub fn set_offline(&mut self) {
553 self.offline = true;
554 }
555
556 pub fn require_bootable(&mut self) {
558 self.require_bootable = true;
559 }
560
561 pub fn set_ostree_version(&mut self, year: u32, v: u32) {
563 self.ostree_v2024_3 = (year > 2024) || (year == 2024 && v >= 3)
564 }
565
566 pub fn disable_gc(&mut self) {
568 self.disable_gc = true;
569 }
570
571 #[context("Preparing import")]
576 pub async fn prepare(&mut self) -> Result<PrepareResult> {
577 self.prepare_internal(false).await
578 }
579
580 pub fn request_progress(&mut self) -> Receiver<ImportProgress> {
582 assert!(self.layer_progress.is_none());
583 let (s, r) = tokio::sync::mpsc::channel(2);
584 self.layer_progress = Some(s);
585 r
586 }
587
588 pub fn request_layer_progress(
590 &mut self,
591 ) -> tokio::sync::watch::Receiver<Option<LayerProgress>> {
592 assert!(self.layer_byte_progress.is_none());
593 let (s, r) = tokio::sync::watch::channel(None);
594 self.layer_byte_progress = Some(s);
595 r
596 }
597
598 #[context("Writing cached pending manifest")]
601 pub(crate) async fn cache_pending(
602 &self,
603 commit: &str,
604 manifest_digest: &Digest,
605 manifest: &ImageManifest,
606 config: &ImageConfiguration,
607 ) -> Result<()> {
608 let commitmeta = glib::VariantDict::new(None);
609 commitmeta.insert(
610 Self::CACHED_KEY_MANIFEST_DIGEST,
611 manifest_digest.to_string(),
612 );
613 let cached_manifest = manifest
614 .to_canon_json_string()
615 .context("Serializing manifest")?;
616 commitmeta.insert(Self::CACHED_KEY_MANIFEST, cached_manifest);
617 let cached_config = config
618 .to_canon_json_string()
619 .context("Serializing config")?;
620 commitmeta.insert(Self::CACHED_KEY_CONFIG, cached_config);
621 let commitmeta = commitmeta.to_variant();
622 let commit = commit.to_string();
624 let repo = self.repo.clone();
625 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
626 repo.write_commit_detached_metadata(&commit, Some(&commitmeta), Some(cancellable))
627 .map_err(anyhow::Error::msg)
628 })
629 .await
630 }
631
632 fn create_prepared_import(
635 &mut self,
636 manifest_digest: Digest,
637 manifest: ImageManifest,
638 config: ImageConfiguration,
639 previous_state: Option<Box<LayeredImageState>>,
640 previous_imageid: Option<String>,
641 proxy_img: OpenedImage,
642 ) -> Result<Box<PreparedImport>> {
643 let config_labels = super::labels_of(&config);
644 if self.require_bootable {
645 let bootable_key = ostree::METADATA_KEY_BOOTABLE;
646 let bootable = config_labels.is_some_and(|l| {
647 l.contains_key(bootable_key.as_str()) || l.contains_key(BOOTC_LABEL)
648 });
649 if !bootable {
650 anyhow::bail!("Target image does not have {bootable_key} label");
651 }
652 let container_arch = config.architecture();
653 let target_arch = &Arch::default();
654 if container_arch != target_arch {
655 anyhow::bail!("Image has architecture {container_arch}; expected {target_arch}");
656 }
657 }
658
659 let (commit_layer, component_layers, remaining_layers) =
660 parse_manifest_layout(&manifest, &config)?;
661
662 let query = |l: &Descriptor| query_layer(&self.repo, l.clone());
663 let commit_layer = commit_layer.map(query).transpose()?;
664 let component_layers = component_layers
665 .into_iter()
666 .map(query)
667 .collect::<Result<Vec<_>>>()?;
668 let remaining_layers = remaining_layers
669 .into_iter()
670 .map(query)
671 .collect::<Result<Vec<_>>>()?;
672
673 let previous_manifest_digest = previous_state.as_ref().map(|s| s.manifest_digest.clone());
674 let imp = PreparedImport {
675 manifest_digest,
676 manifest,
677 config,
678 previous_state,
679 previous_manifest_digest,
680 previous_imageid,
681 ostree_layers: component_layers,
682 ostree_commit_layer: commit_layer,
683 layers: remaining_layers,
684 verify_text: None,
685 proxy_img,
686 };
687 Ok(Box::new(imp))
688 }
689
690 #[context("Fetching manifest")]
692 pub(crate) async fn prepare_internal(&mut self, verify_layers: bool) -> Result<PrepareResult> {
693 match &self.imgref.sigverify {
694 SignatureSource::ContainerPolicy if skopeo::container_policy_is_default_insecure()? => {
695 return Err(anyhow!("containers-policy.json specifies a default of `insecureAcceptAnything`; refusing usage"));
696 }
697 SignatureSource::OstreeRemote(_) if verify_layers => {
698 return Err(anyhow!(
699 "Cannot currently verify layered containers via ostree remote"
700 ));
701 }
702 _ => {}
703 }
704
705 let previous_state = try_query_image(&self.repo, &self.imgref.imgref)?;
707
708 let target_reference = self.imgref.imgref.name.parse::<Reference>().ok();
710 let previous_state = if let Some(target_digest) = target_reference
711 .as_ref()
712 .and_then(|v| v.digest())
713 .map(Digest::from_str)
714 .transpose()?
715 {
716 if let Some(previous_state) = previous_state {
717 if previous_state.manifest_digest == target_digest {
719 tracing::debug!("Digest-based pullspec {:?} already present", self.imgref);
720 return Ok(PrepareResult::AlreadyPresent(previous_state));
721 }
722 Some(previous_state)
723 } else {
724 None
725 }
726 } else {
727 previous_state
728 };
729
730 if self.offline {
731 anyhow::bail!("Manifest fetch required in offline mode");
732 }
733
734 let proxy_img = self
735 .proxy
736 .open_image(&self.imgref.imgref.to_string())
737 .await?;
738
739 let (manifest_digest, manifest) = self.proxy.fetch_manifest(&proxy_img).await?;
740 let manifest_digest = Digest::from_str(&manifest_digest)?;
741 let new_imageid = manifest.config().digest();
742
743 let (previous_state, previous_imageid) = if let Some(previous_state) = previous_state {
746 if previous_state.manifest_digest == manifest_digest {
748 return Ok(PrepareResult::AlreadyPresent(previous_state));
749 }
750 let previous_imageid = previous_state.manifest.config().digest();
752 if previous_imageid == new_imageid {
753 return Ok(PrepareResult::AlreadyPresent(previous_state));
754 }
755 let previous_imageid = previous_imageid.to_string();
756 (Some(previous_state), Some(previous_imageid))
757 } else {
758 (None, None)
759 };
760
761 let config = self.proxy.fetch_config(&proxy_img).await?;
762
763 if let Some(previous_state) = previous_state.as_ref() {
766 self.cache_pending(
767 previous_state.merge_commit.as_str(),
768 &manifest_digest,
769 &manifest,
770 &config,
771 )
772 .await?;
773 }
774
775 let imp = self.create_prepared_import(
776 manifest_digest,
777 manifest,
778 config,
779 previous_state,
780 previous_imageid,
781 proxy_img,
782 )?;
783 Ok(PrepareResult::Ready(imp))
784 }
785
786 #[context("Unencapsulating base")]
788 pub(crate) async fn unencapsulate_base(
789 &self,
790 import: &mut store::PreparedImport,
791 require_ostree: bool,
792 write_refs: bool,
793 ) -> Result<()> {
794 tracing::debug!("Fetching base");
795 if matches!(self.imgref.sigverify, SignatureSource::ContainerPolicy)
796 && skopeo::container_policy_is_default_insecure()?
797 {
798 return Err(anyhow!("containers-policy.json specifies a default of `insecureAcceptAnything`; refusing usage"));
799 }
800 let remote = match &self.imgref.sigverify {
801 SignatureSource::OstreeRemote(remote) => Some(remote.clone()),
802 SignatureSource::ContainerPolicy | SignatureSource::ContainerPolicyAllowInsecure => {
803 None
804 }
805 };
806 let Some(commit_layer) = import.ostree_commit_layer.as_mut() else {
807 if require_ostree {
808 anyhow::bail!(
809 "No {DIFFID_LABEL} label found, not an ostree encapsulated container"
810 );
811 }
812 return Ok(());
813 };
814 let des_layers = self.proxy.get_layer_info(&import.proxy_img).await?;
815 for layer in import.ostree_layers.iter_mut() {
816 if layer.commit.is_some() {
817 continue;
818 }
819 if let Some(p) = self.layer_progress.as_ref() {
820 p.send(ImportProgress::OstreeChunkStarted(layer.layer.clone()))
821 .await?;
822 }
823 let (blob, driver, media_type) = fetch_layer(
824 &self.proxy,
825 &import.proxy_img,
826 &import.manifest,
827 &layer.layer,
828 self.layer_byte_progress.as_ref(),
829 des_layers.as_ref(),
830 self.imgref.imgref.transport,
831 )
832 .await?;
833 let repo = self.repo.clone();
834 let target_ref = layer.ostree_ref.clone();
835 let import_task =
836 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
837 let txn = repo.auto_transaction(Some(cancellable))?;
838 let mut importer = crate::tar::Importer::new_for_object_set(&repo);
839 let blob = tokio_util::io::SyncIoBridge::new(blob);
840 let mut blob = Decompressor::new(&media_type, blob)?;
841 let mut archive = tar::Archive::new(&mut blob);
842 importer.import_objects(&mut archive, Some(cancellable))?;
843 let commit = if write_refs {
844 let commit = importer.finish_import_object_set()?;
845 repo.transaction_set_ref(None, &target_ref, Some(commit.as_str()));
846 tracing::debug!("Wrote {} => {}", target_ref, commit);
847 Some(commit)
848 } else {
849 None
850 };
851 txn.commit(Some(cancellable))?;
852 blob.finish()?;
853 Ok::<_, anyhow::Error>(commit)
854 })
855 .map_err(|e| e.context(format!("Layer {}", layer.layer.digest())));
856 let commit = super::unencapsulate::join_fetch(import_task, driver).await?;
857 layer.commit = commit;
858 if let Some(p) = self.layer_progress.as_ref() {
859 p.send(ImportProgress::OstreeChunkCompleted(layer.layer.clone()))
860 .await?;
861 }
862 }
863 if commit_layer.commit.is_none() {
864 if let Some(p) = self.layer_progress.as_ref() {
865 p.send(ImportProgress::OstreeChunkStarted(
866 commit_layer.layer.clone(),
867 ))
868 .await?;
869 }
870 let (blob, driver, media_type) = fetch_layer(
871 &self.proxy,
872 &import.proxy_img,
873 &import.manifest,
874 &commit_layer.layer,
875 self.layer_byte_progress.as_ref(),
876 des_layers.as_ref(),
877 self.imgref.imgref.transport,
878 )
879 .await?;
880 let repo = self.repo.clone();
881 let target_ref = commit_layer.ostree_ref.clone();
882 let import_task =
883 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
884 let txn = repo.auto_transaction(Some(cancellable))?;
885 let mut importer = crate::tar::Importer::new_for_commit(&repo, remote);
886 let blob = tokio_util::io::SyncIoBridge::new(blob);
887 let mut blob = Decompressor::new(&media_type, blob)?;
888 let mut archive = tar::Archive::new(&mut blob);
889 importer.import_commit(&mut archive, Some(cancellable))?;
890 let (commit, verify_text) = importer.finish_import_commit();
891 if write_refs {
892 repo.transaction_set_ref(None, &target_ref, Some(commit.as_str()));
893 tracing::debug!("Wrote {} => {}", target_ref, commit);
894 }
895 repo.mark_commit_partial(&commit, false)?;
896 txn.commit(Some(cancellable))?;
897 blob.finish()?;
898 Ok::<_, anyhow::Error>((commit, verify_text))
899 });
900 let (commit, verify_text) =
901 super::unencapsulate::join_fetch(import_task, driver).await?;
902 commit_layer.commit = Some(commit);
903 import.verify_text = verify_text;
904 if let Some(p) = self.layer_progress.as_ref() {
905 p.send(ImportProgress::OstreeChunkCompleted(
906 commit_layer.layer.clone(),
907 ))
908 .await?;
909 }
910 };
911 Ok(())
912 }
913
914 pub async fn unencapsulate(mut self) -> Result<Import> {
919 let mut prep = match self.prepare_internal(false).await? {
920 PrepareResult::AlreadyPresent(_) => {
921 panic!("Should not have image present for unencapsulation")
922 }
923 PrepareResult::Ready(r) => r,
924 };
925 if !prep.layers.is_empty() {
926 anyhow::bail!("Image has {} non-ostree layers", prep.layers.len());
927 }
928 let deprecated_warning = prep.deprecated_warning().map(ToOwned::to_owned);
929 self.unencapsulate_base(&mut prep, true, false).await?;
930 self.proxy.close_image(&prep.proxy_img).await?;
933 let ostree_commit = prep.ostree_commit_layer.unwrap().commit.unwrap();
935 let image_digest = prep.manifest_digest;
936 Ok(Import {
937 ostree_commit,
938 image_digest,
939 deprecated_warning,
940 })
941 }
942
943 fn write_merge_commit_impl(
946 repo: &ostree::Repo,
947 base_commit: Option<&str>,
948 layer_commits: &[String],
949 have_derived_layers: bool,
950 metadata: glib::Variant,
951 timestamp: u64,
952 ostree_ref: &str,
953 no_imgref: bool,
954 disable_gc: bool,
955 cancellable: Option<&gio::Cancellable>,
956 ) -> Result<Box<LayeredImageState>> {
957 use rustix::fd::AsRawFd;
958
959 let txn = repo.auto_transaction(cancellable)?;
960
961 let devino = ostree::RepoDevInoCache::new();
962 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
963 let repo_tmp = repodir.open_dir("tmp")?;
964 let td = cap_std_ext::cap_tempfile::TempDir::new_in(&repo_tmp)?;
965
966 let rootpath = "root";
967 let checkout_mode = if repo.mode() == ostree::RepoMode::Bare {
968 ostree::RepoCheckoutMode::None
969 } else {
970 ostree::RepoCheckoutMode::User
971 };
972 let mut checkout_opts = ostree::RepoCheckoutAtOptions {
973 mode: checkout_mode,
974 overwrite_mode: ostree::RepoCheckoutOverwriteMode::UnionFiles,
975 devino_to_csum_cache: Some(devino.clone()),
976 no_copy_fallback: true,
977 force_copy_zerosized: true,
978 process_whiteouts: false,
979 ..Default::default()
980 };
981 if let Some(base) = base_commit.as_ref() {
982 repo.checkout_at(
983 Some(&checkout_opts),
984 (*td).as_raw_fd(),
985 rootpath,
986 &base,
987 cancellable,
988 )
989 .context("Checking out base commit")?;
990 }
991
992 checkout_opts.process_whiteouts = true;
994 for commit in layer_commits {
995 tracing::debug!("Unpacking {commit}");
996 repo.checkout_at(
997 Some(&checkout_opts),
998 (*td).as_raw_fd(),
999 rootpath,
1000 &commit,
1001 cancellable,
1002 )
1003 .with_context(|| format!("Checking out layer {commit}"))?;
1004 }
1005
1006 let root_dir = td.open_dir(rootpath)?;
1007
1008 let modifier =
1009 ostree::RepoCommitModifier::new(ostree::RepoCommitModifierFlags::empty(), None);
1010 modifier.set_devino_cache(&devino);
1011 if have_derived_layers {
1015 let sepolicy = ostree::SePolicy::new_at(root_dir.as_raw_fd(), cancellable)?;
1016 tracing::debug!("labeling from merged tree");
1017 modifier.set_sepolicy(Some(&sepolicy));
1018 } else if let Some(base) = base_commit.as_ref() {
1019 tracing::debug!("labeling from base tree");
1020 modifier.set_sepolicy_from_commit(repo, &base, cancellable)?;
1022 } else {
1023 panic!("Unexpected state: no derived layers and no base")
1024 }
1025
1026 cleanup_root(&root_dir)?;
1027
1028 let mt = ostree::MutableTree::new();
1029 repo.write_dfd_to_mtree(
1030 (*td).as_raw_fd(),
1031 rootpath,
1032 &mt,
1033 Some(&modifier),
1034 cancellable,
1035 )
1036 .context("Writing merged filesystem to mtree")?;
1037
1038 let merged_root = repo
1039 .write_mtree(&mt, cancellable)
1040 .context("Writing mtree")?;
1041 let merged_root = merged_root.downcast::<ostree::RepoFile>().unwrap();
1042 let parent = base_commit.as_deref();
1045 let merged_commit = repo
1046 .write_commit_with_time(
1047 parent,
1048 None,
1049 None,
1050 Some(&metadata),
1051 &merged_root,
1052 timestamp,
1053 cancellable,
1054 )
1055 .context("Writing commit")?;
1056 if !no_imgref {
1057 repo.transaction_set_ref(None, ostree_ref, Some(merged_commit.as_str()));
1058 }
1059 txn.commit(cancellable)?;
1060
1061 if !disable_gc {
1062 let n: u32 = gc_image_layers_impl(repo, cancellable)?;
1063 tracing::debug!("pruned {n} layers");
1064 }
1065
1066 let state = query_image_commit(repo, &merged_commit)?;
1069 Ok(state)
1070 }
1071
1072 #[context("Importing")]
1076 pub async fn import(
1077 mut self,
1078 mut import: Box<PreparedImport>,
1079 ) -> Result<Box<LayeredImageState>> {
1080 if let Some(status) = import.format_layer_status() {
1081 system_repo_journal_print(&self.repo, libsystemd::logging::Priority::Info, &status);
1082 }
1083 self.unencapsulate_base(&mut import, false, true).await?;
1086 let des_layers = self.proxy.get_layer_info(&import.proxy_img).await?;
1087 let proxy = self.proxy;
1088 let target_imgref = self.target_imgref.as_ref().unwrap_or(&self.imgref);
1089 let base_commit = import
1090 .ostree_commit_layer
1091 .as_ref()
1092 .map(|c| c.commit.clone().unwrap());
1093
1094 let root_is_transient = if let Some(base) = base_commit.as_ref() {
1095 let rootf = self.repo.read_commit(&base, gio::Cancellable::NONE)?.0;
1096 let rootf = rootf.downcast_ref::<ostree::RepoFile>().unwrap();
1097 crate::ostree_prepareroot::overlayfs_root_enabled(rootf)?
1098 } else {
1099 true
1101 };
1102 tracing::debug!("Base rootfs is transient: {root_is_transient}");
1103
1104 let ostree_ref = ref_for_image(&target_imgref.imgref)?;
1105
1106 let mut layer_commits = Vec::new();
1107 let mut layer_filtered_content: Option<MetaFilteredData> = None;
1108 let have_derived_layers = !import.layers.is_empty();
1109 tracing::debug!("Processing layers: {}", import.layers.len());
1110 for layer in import.layers {
1111 if let Some(c) = layer.commit {
1112 tracing::debug!("Reusing fetched commit {}", c);
1113 layer_commits.push(c.to_string());
1114 } else {
1115 if let Some(p) = self.layer_progress.as_ref() {
1116 p.send(ImportProgress::DerivedLayerStarted(layer.layer.clone()))
1117 .await?;
1118 }
1119 let (blob, driver, media_type) = super::unencapsulate::fetch_layer(
1120 &proxy,
1121 &import.proxy_img,
1122 &import.manifest,
1123 &layer.layer,
1124 self.layer_byte_progress.as_ref(),
1125 des_layers.as_ref(),
1126 self.imgref.imgref.transport,
1127 )
1128 .await?;
1129 let opts = crate::tar::WriteTarOptions {
1132 base: base_commit.clone(),
1133 selinux: true,
1134 allow_nonusr: root_is_transient,
1135 retain_var: self.ostree_v2024_3,
1136 };
1137 let r = crate::tar::write_tar(
1138 &self.repo,
1139 blob,
1140 media_type,
1141 layer.ostree_ref.as_str(),
1142 Some(opts),
1143 );
1144 let r = super::unencapsulate::join_fetch(r, driver)
1145 .await
1146 .with_context(|| format!("Parsing layer blob {}", layer.layer.digest()))?;
1147 tracing::debug!("Imported layer: {}", r.commit.as_str());
1148 layer_commits.push(r.commit);
1149 let filtered_owned = HashMap::from_iter(r.filtered.clone());
1150 if let Some((filtered, n_rest)) = bootc_utils::collect_until(
1151 r.filtered.iter(),
1152 const { NonZeroUsize::new(5).unwrap() },
1153 ) {
1154 let mut msg = String::new();
1155 for (path, n) in filtered {
1156 write!(msg, "{path}: {n} ").unwrap();
1157 }
1158 if n_rest > 0 {
1159 write!(msg, "...and {n_rest} more").unwrap();
1160 }
1161 tracing::debug!("Found filtered toplevels: {msg}");
1162 layer_filtered_content
1163 .get_or_insert_default()
1164 .insert(layer.layer.digest().to_string(), filtered_owned);
1165 } else {
1166 tracing::debug!("No filtered content");
1167 }
1168 if let Some(p) = self.layer_progress.as_ref() {
1169 p.send(ImportProgress::DerivedLayerCompleted(layer.layer.clone()))
1170 .await?;
1171 }
1172 }
1173 }
1174
1175 proxy.close_image(&import.proxy_img).await?;
1178
1179 proxy.finalize().await?;
1181 tracing::debug!("finalized proxy");
1182
1183 let _ = self.layer_byte_progress.take();
1185 let _ = self.layer_progress.take();
1186
1187 let mut metadata = BTreeMap::new();
1188 metadata.insert(
1189 META_MANIFEST_DIGEST,
1190 import.manifest_digest.to_string().to_variant(),
1191 );
1192 metadata.insert(
1193 META_MANIFEST,
1194 import.manifest.to_canon_json_string()?.to_variant(),
1195 );
1196 metadata.insert(
1197 META_CONFIG,
1198 import.config.to_canon_json_string()?.to_variant(),
1199 );
1200 metadata.insert(
1201 "ostree.importer.version",
1202 env!("CARGO_PKG_VERSION").to_variant(),
1203 );
1204 let metadata = metadata.to_variant();
1205
1206 let timestamp = timestamp_of_manifest_or_config(&import.manifest, &import.config)
1207 .unwrap_or_else(|| chrono::offset::Utc::now().timestamp() as u64);
1208 let repo = self.repo;
1210 let mut state = crate::tokio_util::spawn_blocking_cancellable_flatten(
1211 move |cancellable| -> Result<Box<LayeredImageState>> {
1212 Self::write_merge_commit_impl(
1213 &repo,
1214 base_commit.as_deref(),
1215 &layer_commits,
1216 have_derived_layers,
1217 metadata,
1218 timestamp,
1219 &ostree_ref,
1220 self.no_imgref,
1221 self.disable_gc,
1222 Some(cancellable),
1223 )
1224 },
1225 )
1226 .await?;
1227 state.verify_text = import.verify_text;
1229 state.filtered_files = layer_filtered_content;
1230 Ok(state)
1231 }
1232}
1233
1234pub fn list_images(repo: &ostree::Repo) -> Result<Vec<String>> {
1236 let cancellable = gio::Cancellable::NONE;
1237 let refs = repo.list_refs_ext(
1238 Some(IMAGE_PREFIX),
1239 ostree::RepoListRefsExtFlags::empty(),
1240 cancellable,
1241 )?;
1242 refs.keys()
1243 .map(|imgname| refescape::unprefix_unescape_ref(IMAGE_PREFIX, imgname))
1244 .collect()
1245}
1246
1247fn try_query_image(
1250 repo: &ostree::Repo,
1251 imgref: &ImageReference,
1252) -> Result<Option<Box<LayeredImageState>>> {
1253 let ostree_ref = &ref_for_image(imgref)?;
1254 if let Some(merge_rev) = repo.resolve_rev(ostree_ref, true)? {
1255 match query_image_commit(repo, merge_rev.as_str()) {
1256 Ok(r) => Ok(Some(r)),
1257 Err(e) => {
1258 eprintln!("error: failed to query image commit: {e}");
1259 Ok(None)
1260 }
1261 }
1262 } else {
1263 Ok(None)
1264 }
1265}
1266
1267#[context("Querying image {imgref}")]
1269pub fn query_image(
1270 repo: &ostree::Repo,
1271 imgref: &ImageReference,
1272) -> Result<Option<Box<LayeredImageState>>> {
1273 let ostree_ref = &ref_for_image(imgref)?;
1274 let merge_rev = repo.resolve_rev(ostree_ref, true)?;
1275 merge_rev
1276 .map(|r| query_image_commit(repo, r.as_str()))
1277 .transpose()
1278}
1279
1280fn parse_cached_update(meta: &glib::VariantDict) -> Result<Option<CachedImageUpdate>> {
1282 let manifest_digest =
1284 if let Some(d) = meta.lookup::<String>(ImageImporter::CACHED_KEY_MANIFEST_DIGEST)? {
1285 d
1286 } else {
1287 return Ok(None);
1290 };
1291 let manifest_digest = Digest::from_str(&manifest_digest)?;
1292 let manifest = meta.lookup_value(ImageImporter::CACHED_KEY_MANIFEST, None);
1295 let manifest: oci_image::ImageManifest = manifest
1296 .as_ref()
1297 .and_then(|v| v.str())
1298 .map(serde_json::from_str)
1299 .transpose()?
1300 .ok_or_else(|| {
1301 anyhow!(
1302 "Expected cached manifest {}",
1303 ImageImporter::CACHED_KEY_MANIFEST
1304 )
1305 })?;
1306 let config = meta.lookup_value(ImageImporter::CACHED_KEY_CONFIG, None);
1307 let config: oci_image::ImageConfiguration = config
1308 .as_ref()
1309 .and_then(|v| v.str())
1310 .map(serde_json::from_str)
1311 .transpose()?
1312 .ok_or_else(|| {
1313 anyhow!(
1314 "Expected cached manifest {}",
1315 ImageImporter::CACHED_KEY_CONFIG
1316 )
1317 })?;
1318 Ok(Some(CachedImageUpdate {
1319 manifest,
1320 config,
1321 manifest_digest,
1322 }))
1323}
1324
1325#[context("Clearing cached update {imgref}")]
1327pub fn clear_cached_update(repo: &ostree::Repo, imgref: &ImageReference) -> Result<()> {
1328 let cancellable = gio::Cancellable::NONE;
1329 let ostree_ref = ref_for_image(imgref)?;
1330 let rev = repo.require_rev(&ostree_ref)?;
1331 let Some(commitmeta) = repo.read_commit_detached_metadata(&rev, cancellable)? else {
1332 return Ok(());
1333 };
1334
1335 let mut commitmeta: BTreeMap<String, glib::Variant> =
1337 BTreeMap::from_variant(&commitmeta).unwrap();
1338 let mut changed = false;
1339 for key in [
1340 ImageImporter::CACHED_KEY_CONFIG,
1341 ImageImporter::CACHED_KEY_MANIFEST,
1342 ImageImporter::CACHED_KEY_MANIFEST_DIGEST,
1343 ] {
1344 if commitmeta.remove(key).is_some() {
1345 changed = true;
1346 }
1347 }
1348 if !changed {
1349 return Ok(());
1350 }
1351 let commitmeta = glib::Variant::from(commitmeta);
1352 repo.write_commit_detached_metadata(&rev, Some(&commitmeta), cancellable)?;
1353 Ok(())
1354}
1355
1356pub fn query_image_commit(repo: &ostree::Repo, commit: &str) -> Result<Box<LayeredImageState>> {
1359 let merge_commit = commit.to_string();
1360 let merge_commit_obj = repo.load_commit(commit)?.0;
1361 let commit_meta = &merge_commit_obj.child_value(0);
1362 let commit_meta = &ostree::glib::VariantDict::new(Some(commit_meta));
1363 let (manifest, manifest_digest) = manifest_data_from_commitmeta(commit_meta)?;
1364 let configuration = image_config_from_commitmeta(commit_meta)?;
1365 let mut layers = manifest.layers().iter().cloned();
1366 let base_layer = layers.next().ok_or_else(|| anyhow!("No layers found"))?;
1368 let base_layer = query_layer(repo, base_layer)?;
1369 let ostree_ref = base_layer.ostree_ref.as_str();
1370 let base_commit = base_layer
1371 .commit
1372 .ok_or_else(|| anyhow!("Missing base image ref {ostree_ref}"))?;
1373
1374 let detached_commitmeta =
1375 repo.read_commit_detached_metadata(&merge_commit, gio::Cancellable::NONE)?;
1376 let detached_commitmeta = detached_commitmeta
1377 .as_ref()
1378 .map(|v| glib::VariantDict::new(Some(v)));
1379 let cached_update = detached_commitmeta
1380 .as_ref()
1381 .map(parse_cached_update)
1382 .transpose()?
1383 .flatten();
1384 let state = Box::new(LayeredImageState {
1385 base_commit,
1386 merge_commit,
1387 manifest_digest,
1388 manifest,
1389 configuration,
1390 cached_update,
1391 verify_text: None,
1393 filtered_files: None,
1394 });
1395 tracing::debug!("Wrote merge commit {}", state.merge_commit);
1396 Ok(state)
1397}
1398
1399fn manifest_for_image(repo: &ostree::Repo, imgref: &ImageReference) -> Result<ImageManifest> {
1400 let ostree_ref = ref_for_image(imgref)?;
1401 let rev = repo.require_rev(&ostree_ref)?;
1402 let (commit_obj, _) = repo.load_commit(rev.as_str())?;
1403 let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
1404 Ok(manifest_data_from_commitmeta(commit_meta)?.0)
1405}
1406
1407#[context("Copying image")]
1410pub async fn copy(
1411 src_repo: &ostree::Repo,
1412 src_imgref: &ImageReference,
1413 dest_repo: &ostree::Repo,
1414 dest_imgref: &ImageReference,
1415) -> Result<()> {
1416 let src_ostree_ref = ref_for_image(src_imgref)?;
1417 let src_commit = src_repo.require_rev(&src_ostree_ref)?;
1418 let manifest = manifest_for_image(src_repo, src_imgref)?;
1419 let layer_refs = manifest
1421 .layers()
1422 .iter()
1423 .map(ref_for_layer)
1424 .chain(std::iter::once(Ok(src_commit.to_string())));
1425 for ostree_ref in layer_refs {
1426 let ostree_ref = ostree_ref?;
1427 let src_repo = src_repo.clone();
1428 let dest_repo = dest_repo.clone();
1429 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| -> Result<_> {
1430 let cancellable = Some(cancellable);
1431 let srcfd = &format!("file:///proc/self/fd/{}", src_repo.dfd());
1432 let flags = ostree::RepoPullFlags::MIRROR;
1433 let opts = glib::VariantDict::new(None);
1434 let refs = [ostree_ref.as_str()];
1435 opts.insert("disable-verify-bindings", true);
1437 opts.insert("refs", &refs[..]);
1438 opts.insert("flags", flags.bits() as i32);
1439 let options = opts.to_variant();
1440 dest_repo.pull_with_options(srcfd, &options, None, cancellable)?;
1441 Ok(())
1442 })
1443 .await?;
1444 }
1445
1446 let dest_ostree_ref = ref_for_image(dest_imgref)?;
1447 dest_repo.set_ref_immediate(
1448 None,
1449 &dest_ostree_ref,
1450 Some(&src_commit),
1451 gio::Cancellable::NONE,
1452 )?;
1453
1454 Ok(())
1455}
1456
1457#[derive(Clone, Debug, Default)]
1459#[non_exhaustive]
1460pub struct ExportToOCIOpts {
1461 pub skip_compression: bool,
1463 pub authfile: Option<std::path::PathBuf>,
1465 pub progress_to_stdout: bool,
1467}
1468
1469fn chunking_from_layer_committed(
1474 repo: &ostree::Repo,
1475 l: &Descriptor,
1476 chunking: &mut chunking::Chunking,
1477) -> Result<()> {
1478 let mut chunk = Chunk::default();
1479 let layer_ref = &ref_for_layer(l)?;
1480 let root = repo.read_commit(layer_ref, gio::Cancellable::NONE)?.0;
1481 let e = root.enumerate_children(
1482 "standard::name,standard::size",
1483 gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS,
1484 gio::Cancellable::NONE,
1485 )?;
1486 for child in e.clone() {
1487 let child = &child?;
1488 let name = child.name();
1490 let name = Utf8Path::from_path(&name).unwrap();
1492 ostree::validate_checksum_string(name.as_str())?;
1493 chunking.remainder.move_obj(&mut chunk, name.as_str());
1494 }
1495 chunking.chunks.push(chunk);
1496 Ok(())
1497}
1498
1499#[context("Copying image")]
1501pub(crate) fn export_to_oci(
1502 repo: &ostree::Repo,
1503 imgref: &ImageReference,
1504 dest_oci: &Dir,
1505 tag: Option<&str>,
1506 opts: ExportToOCIOpts,
1507) -> Result<Descriptor> {
1508 let srcinfo = query_image(repo, imgref)?.ok_or_else(|| anyhow!("No such image"))?;
1509 let (commit_layer, component_layers, remaining_layers) =
1510 parse_manifest_layout(&srcinfo.manifest, &srcinfo.configuration)?;
1511
1512 let mut new_manifest = srcinfo.manifest.clone();
1516 new_manifest.layers_mut().clear();
1517 let mut new_config = srcinfo.configuration.clone();
1518 if let Some(history) = new_config.history_mut() {
1519 history.clear();
1520 }
1521 new_config.rootfs_mut().diff_ids_mut().clear();
1522
1523 let opts = ExportOpts {
1524 skip_compression: opts.skip_compression,
1525 authfile: opts.authfile,
1526 ..Default::default()
1527 };
1528
1529 let mut labels = HashMap::new();
1530
1531 let mut dest_oci = ocidir::OciDir::ensure(dest_oci.try_clone()?)?;
1532
1533 let commit_chunk_ref = commit_layer
1534 .as_ref()
1535 .map(|l| ref_for_layer(l))
1536 .transpose()?;
1537 let commit_chunk_rev = commit_chunk_ref
1538 .as_ref()
1539 .map(|r| repo.require_rev(&r))
1540 .transpose()?;
1541 if let Some(commit_chunk_rev) = commit_chunk_rev {
1542 let mut chunking = chunking::Chunking::new(repo, &commit_chunk_rev)?;
1543 for layer in component_layers {
1544 chunking_from_layer_committed(repo, layer, &mut chunking)?;
1545 }
1546
1547 export_chunked(
1550 repo,
1551 &srcinfo.base_commit,
1552 &mut dest_oci,
1553 &mut new_manifest,
1554 &mut new_config,
1555 &mut labels,
1556 chunking,
1557 &opts,
1558 "",
1559 )?;
1560 }
1561
1562 let compression = opts.skip_compression.then_some(Compression::none());
1564 for (i, layer) in remaining_layers.iter().enumerate() {
1565 let layer_ref = &ref_for_layer(layer)?;
1566 let mut target_blob = dest_oci.create_gzip_layer(compression)?;
1567 let export_opts = crate::tar::ExportOptions { raw: true };
1569 crate::tar::export_commit(
1570 repo,
1571 layer_ref.as_str(),
1572 &mut target_blob,
1573 Some(export_opts),
1574 )?;
1575 let layer = target_blob.complete()?;
1576 let previous_annotations = srcinfo
1577 .manifest
1578 .layers()
1579 .get(i)
1580 .and_then(|l| l.annotations().as_ref())
1581 .cloned();
1582 let history = srcinfo.configuration.history().as_ref();
1583 let history_entry = history.and_then(|v| v.get(i));
1584 let previous_description = history_entry
1585 .clone()
1586 .and_then(|h| h.comment().as_deref())
1587 .unwrap_or_default();
1588
1589 let previous_created = history_entry
1590 .and_then(|h| h.created().as_deref())
1591 .and_then(bootc_utils::try_deserialize_timestamp)
1592 .unwrap_or_default();
1593
1594 dest_oci.push_layer_full(
1595 &mut new_manifest,
1596 &mut new_config,
1597 layer,
1598 previous_annotations,
1599 previous_description,
1600 previous_created,
1601 )
1602 }
1603
1604 let new_config = dest_oci.write_config(new_config)?;
1605 new_manifest.set_config(new_config);
1606
1607 Ok(dest_oci.insert_manifest(new_manifest, tag, oci_image::Platform::default())?)
1608}
1609
1610#[context("Export")]
1613pub async fn export(
1614 repo: &ostree::Repo,
1615 src_imgref: &ImageReference,
1616 dest_imgref: &ImageReference,
1617 opts: Option<ExportToOCIOpts>,
1618) -> Result<oci_image::Digest> {
1619 let opts = opts.unwrap_or_default();
1620 let target_oci = dest_imgref.transport == Transport::OciDir;
1621 let tempdir = if !target_oci {
1622 let vartmp = cap_std::fs::Dir::open_ambient_dir("/var/tmp", cap_std::ambient_authority())?;
1623 let td = cap_std_ext::cap_tempfile::TempDir::new_in(&vartmp)?;
1624 let opts = ExportToOCIOpts {
1626 skip_compression: true,
1627 progress_to_stdout: opts.progress_to_stdout,
1628 ..Default::default()
1629 };
1630 export_to_oci(repo, src_imgref, &td, None, opts)?;
1631 td
1632 } else {
1633 let (path, tag) = parse_oci_path_and_tag(dest_imgref.name.as_str());
1634 tracing::debug!("using OCI path={path} tag={tag:?}");
1635 let path = Dir::open_ambient_dir(path, cap_std::ambient_authority())
1636 .with_context(|| format!("Opening {path}"))?;
1637 let descriptor = export_to_oci(repo, src_imgref, &path, tag, opts)?;
1638 return Ok(descriptor.digest().clone());
1639 };
1640 let target_fd = 3i32;
1642 let tempoci = ImageReference {
1643 transport: Transport::OciDir,
1644 name: format!("/proc/self/fd/{target_fd}"),
1645 };
1646 let authfile = opts.authfile.as_deref();
1647 skopeo::copy(
1648 &tempoci,
1649 dest_imgref,
1650 authfile,
1651 Some((std::sync::Arc::new(tempdir.try_clone()?.into()), target_fd)),
1652 opts.progress_to_stdout,
1653 )
1654 .await
1655}
1656
1657#[context("Listing deployment manifests")]
1660fn list_container_deployment_manifests(
1661 repo: &ostree::Repo,
1662 cancellable: Option<&gio::Cancellable>,
1663) -> Result<Vec<ImageManifest>> {
1664 let commits = OSTREE_BASE_DEPLOYMENT_REFS
1667 .iter()
1668 .chain(RPMOSTREE_BASE_REFS)
1669 .chain(std::iter::once(&BASE_IMAGE_PREFIX))
1670 .try_fold(
1671 std::collections::HashSet::new(),
1672 |mut acc, &p| -> Result<_> {
1673 let refs = repo.list_refs_ext(
1674 Some(p),
1675 ostree::RepoListRefsExtFlags::empty(),
1676 cancellable,
1677 )?;
1678 for (_, v) in refs {
1679 acc.insert(v);
1680 }
1681 Ok(acc)
1682 },
1683 )?;
1684 let mut r = Vec::new();
1686 for commit in commits {
1687 let commit_obj = repo.load_commit(&commit)?.0;
1688 let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
1689 if commit_meta
1690 .lookup::<String>(META_MANIFEST_DIGEST)?
1691 .is_some()
1692 {
1693 tracing::trace!("Commit {commit} is a container image");
1694 let manifest = manifest_data_from_commitmeta(commit_meta)?.0;
1695 r.push(manifest);
1696 }
1697 }
1698 Ok(r)
1699}
1700
1701pub fn gc_image_layers(repo: &ostree::Repo) -> Result<u32> {
1707 gc_image_layers_impl(repo, gio::Cancellable::NONE)
1708}
1709
1710#[context("Pruning image layers")]
1711fn gc_image_layers_impl(
1712 repo: &ostree::Repo,
1713 cancellable: Option<&gio::Cancellable>,
1714) -> Result<u32> {
1715 let all_images = list_images(repo)?;
1716 let deployment_commits = list_container_deployment_manifests(repo, cancellable)?;
1717 let all_manifests = all_images
1718 .into_iter()
1719 .map(|img| {
1720 ImageReference::try_from(img.as_str()).and_then(|ir| manifest_for_image(repo, &ir))
1721 })
1722 .chain(deployment_commits.into_iter().map(Ok))
1723 .collect::<Result<Vec<_>>>()?;
1724 tracing::debug!("Images found: {}", all_manifests.len());
1725 let mut referenced_layers = BTreeSet::new();
1726 for m in all_manifests.iter() {
1727 for layer in m.layers() {
1728 referenced_layers.insert(layer.digest().to_string());
1729 }
1730 }
1731 tracing::debug!("Referenced layers: {}", referenced_layers.len());
1732 let found_layers = repo
1733 .list_refs_ext(
1734 Some(LAYER_PREFIX),
1735 ostree::RepoListRefsExtFlags::empty(),
1736 cancellable,
1737 )?
1738 .into_iter()
1739 .map(|v| v.0);
1740 tracing::debug!("Found layers: {}", found_layers.len());
1741 let mut pruned = 0u32;
1742 for layer_ref in found_layers {
1743 let layer_digest = refescape::unprefix_unescape_ref(LAYER_PREFIX, &layer_ref)?;
1744 if referenced_layers.remove(layer_digest.as_str()) {
1745 continue;
1746 }
1747 pruned += 1;
1748 tracing::debug!("Pruning: {}", layer_ref.as_str());
1749 repo.set_ref_immediate(None, layer_ref.as_str(), None, cancellable)?;
1750 }
1751
1752 Ok(pruned)
1753}
1754
1755#[cfg(feature = "internal-testing-api")]
1756pub fn count_layer_references(repo: &ostree::Repo) -> Result<u32> {
1758 let cancellable = gio::Cancellable::NONE;
1759 let n = repo
1760 .list_refs_ext(
1761 Some(LAYER_PREFIX),
1762 ostree::RepoListRefsExtFlags::empty(),
1763 cancellable,
1764 )?
1765 .len();
1766 Ok(n as u32)
1767}
1768
1769pub fn image_filtered_content_warning(
1771 filtered_files: &Option<MetaFilteredData>,
1772) -> Result<Option<String>> {
1773 use std::fmt::Write;
1774
1775 let r = filtered_files.as_ref().map(|v| {
1776 let mut filtered = BTreeMap::<&String, u32>::new();
1777 for paths in v.values() {
1778 for (k, v) in paths {
1779 let e = filtered.entry(k).or_default();
1780 *e += v;
1781 }
1782 }
1783 let mut buf = "Image contains non-ostree compatible file paths:".to_string();
1784 for (k, v) in filtered {
1785 write!(buf, " {k}: {v}").unwrap();
1786 }
1787 buf
1788 });
1789 Ok(r)
1790}
1791
1792#[context("Pruning {img}")]
1799pub fn remove_image(repo: &ostree::Repo, img: &ImageReference) -> Result<bool> {
1800 let ostree_ref = &ref_for_image(img)?;
1801 let found = repo.resolve_rev(ostree_ref, true)?.is_some();
1802 if found {
1805 repo.set_ref_immediate(None, ostree_ref, None, gio::Cancellable::NONE)?;
1806 }
1807 Ok(found)
1808}
1809
1810pub fn remove_images<'a>(
1817 repo: &ostree::Repo,
1818 imgs: impl IntoIterator<Item = &'a ImageReference>,
1819) -> Result<()> {
1820 let mut missing = Vec::new();
1821 for img in imgs.into_iter() {
1822 let found = remove_image(repo, img)?;
1823 if !found {
1824 missing.push(img);
1825 }
1826 }
1827 if !missing.is_empty() {
1828 let missing = missing.into_iter().fold("".to_string(), |mut a, v| {
1829 a.push_str(&v.to_string());
1830 a
1831 });
1832 return Err(anyhow::anyhow!("Missing images: {missing}"));
1833 }
1834 Ok(())
1835}
1836
1837#[derive(Debug, Default)]
1838struct CompareState {
1839 verified: BTreeSet<Utf8PathBuf>,
1840 inode_corrupted: BTreeSet<Utf8PathBuf>,
1841 unknown_corrupted: BTreeSet<Utf8PathBuf>,
1842}
1843
1844impl CompareState {
1845 fn is_ok(&self) -> bool {
1846 self.inode_corrupted.is_empty() && self.unknown_corrupted.is_empty()
1847 }
1848}
1849
1850fn compare_file_info(src: &gio::FileInfo, target: &gio::FileInfo) -> bool {
1851 if src.file_type() != target.file_type() {
1852 return false;
1853 }
1854 if src.size() != target.size() {
1855 return false;
1856 }
1857 for attr in ["unix::uid", "unix::gid", "unix::mode"] {
1858 if src.attribute_uint32(attr) != target.attribute_uint32(attr) {
1859 return false;
1860 }
1861 }
1862 true
1863}
1864
1865#[context("Querying object inode")]
1866fn inode_of_object(repo: &ostree::Repo, checksum: &str) -> Result<u64> {
1867 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
1868 let (prefix, suffix) = checksum.split_at(2);
1869 let objpath = format!("objects/{prefix}/{suffix}.file");
1870 let metadata = repodir.symlink_metadata(objpath)?;
1871 Ok(metadata.ino())
1872}
1873
1874fn compare_commit_trees(
1875 repo: &ostree::Repo,
1876 root: &Utf8Path,
1877 target: &ostree::RepoFile,
1878 expected: &ostree::RepoFile,
1879 exact: bool,
1880 colliding_inodes: &BTreeSet<u64>,
1881 state: &mut CompareState,
1882) -> Result<()> {
1883 let cancellable = gio::Cancellable::NONE;
1884 let queryattrs = "standard::name,standard::type";
1885 let queryflags = gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS;
1886 let expected_iter = expected.enumerate_children(queryattrs, queryflags, cancellable)?;
1887
1888 while let Some(expected_info) = expected_iter.next_file(cancellable)? {
1889 let expected_child = expected_iter.child(&expected_info);
1890 let name = expected_info.name();
1891 let name = name.to_str().expect("UTF-8 ostree name");
1892 let path = Utf8PathBuf::from(format!("{root}{name}"));
1893 let target_child = target.child(name);
1894 let target_info = crate::diff::query_info_optional(&target_child, queryattrs, queryflags)
1895 .context("querying optional to")?;
1896 let is_dir = matches!(expected_info.file_type(), gio::FileType::Directory);
1897 if let Some(target_info) = target_info {
1898 let to_child = target_child
1899 .downcast::<ostree::RepoFile>()
1900 .expect("downcast");
1901 to_child.ensure_resolved()?;
1902 let from_child = expected_child
1903 .downcast::<ostree::RepoFile>()
1904 .expect("downcast");
1905 from_child.ensure_resolved()?;
1906
1907 if is_dir {
1908 let from_contents_checksum = from_child.tree_get_contents_checksum();
1909 let to_contents_checksum = to_child.tree_get_contents_checksum();
1910 if from_contents_checksum != to_contents_checksum {
1911 let subpath = Utf8PathBuf::from(format!("{path}/"));
1912 compare_commit_trees(
1913 repo,
1914 &subpath,
1915 &from_child,
1916 &to_child,
1917 exact,
1918 colliding_inodes,
1919 state,
1920 )?;
1921 }
1922 } else {
1923 let from_checksum = from_child.checksum();
1924 let to_checksum = to_child.checksum();
1925 let matches = if exact {
1926 from_checksum == to_checksum
1927 } else {
1928 compare_file_info(&target_info, &expected_info)
1929 };
1930 if !matches {
1931 let from_inode = inode_of_object(repo, &from_checksum)?;
1932 let to_inode = inode_of_object(repo, &to_checksum)?;
1933 if colliding_inodes.contains(&from_inode)
1934 || colliding_inodes.contains(&to_inode)
1935 {
1936 state.inode_corrupted.insert(path);
1937 } else {
1938 state.unknown_corrupted.insert(path);
1939 }
1940 } else {
1941 state.verified.insert(path);
1942 }
1943 }
1944 } else {
1945 eprintln!("Missing {path}");
1946 state.unknown_corrupted.insert(path);
1947 }
1948 }
1949 Ok(())
1950}
1951
1952#[context("Verifying container image state")]
1953pub(crate) fn verify_container_image(
1954 sysroot: &SysrootLock,
1955 imgref: &ImageReference,
1956 state: &LayeredImageState,
1957 colliding_inodes: &BTreeSet<u64>,
1958 verbose: bool,
1959) -> Result<bool> {
1960 let cancellable = gio::Cancellable::NONE;
1961 let repo = &sysroot.repo();
1962 let merge_commit = state.merge_commit.as_str();
1963 let merge_commit_root = repo.read_commit(merge_commit, gio::Cancellable::NONE)?.0;
1964 let merge_commit_root = merge_commit_root
1965 .downcast::<ostree::RepoFile>()
1966 .expect("downcast");
1967 merge_commit_root.ensure_resolved()?;
1968
1969 let (commit_layer, _component_layers, remaining_layers) =
1970 parse_manifest_layout(&state.manifest, &state.configuration)?;
1971
1972 let mut comparison_state = CompareState::default();
1973
1974 let query = |l: &Descriptor| query_layer(repo, l.clone());
1975
1976 let base_tree = repo
1977 .read_commit(&state.base_commit, cancellable)?
1978 .0
1979 .downcast::<ostree::RepoFile>()
1980 .expect("downcast");
1981 if let Some(commit_layer) = commit_layer {
1982 println!(
1983 "Verifying with base ostree layer {}",
1984 ref_for_layer(commit_layer)?
1985 );
1986 }
1987 compare_commit_trees(
1988 repo,
1989 "/".into(),
1990 &merge_commit_root,
1991 &base_tree,
1992 true,
1993 colliding_inodes,
1994 &mut comparison_state,
1995 )?;
1996
1997 let remaining_layers = remaining_layers
1998 .into_iter()
1999 .map(query)
2000 .collect::<Result<Vec<_>>>()?;
2001
2002 println!("Image has {} derived layers", remaining_layers.len());
2003
2004 for layer in remaining_layers.iter().rev() {
2005 let layer_ref = layer.ostree_ref.as_str();
2006 let layer_commit = layer
2007 .commit
2008 .as_deref()
2009 .ok_or_else(|| anyhow!("Missing layer {layer_ref}"))?;
2010 let layer_tree = repo
2011 .read_commit(layer_commit, cancellable)?
2012 .0
2013 .downcast::<ostree::RepoFile>()
2014 .expect("downcast");
2015 compare_commit_trees(
2016 repo,
2017 "/".into(),
2018 &merge_commit_root,
2019 &layer_tree,
2020 false,
2021 colliding_inodes,
2022 &mut comparison_state,
2023 )?;
2024 }
2025
2026 let n_verified = comparison_state.verified.len();
2027 if comparison_state.is_ok() {
2028 println!("OK image {imgref} (verified={n_verified})");
2029 println!();
2030 } else {
2031 let n_inode = comparison_state.inode_corrupted.len();
2032 let n_other = comparison_state.unknown_corrupted.len();
2033 eprintln!("warning: Found corrupted merge commit");
2034 eprintln!(" inode clashes: {n_inode}");
2035 eprintln!(" unknown: {n_other}");
2036 eprintln!(" ok: {n_verified}");
2037 if verbose {
2038 eprintln!("Mismatches:");
2039 for path in comparison_state.inode_corrupted {
2040 eprintln!(" inode: {path}");
2041 }
2042 for path in comparison_state.unknown_corrupted {
2043 eprintln!(" other: {path}");
2044 }
2045 }
2046 eprintln!();
2047 return Ok(false);
2048 }
2049
2050 Ok(true)
2051}
2052
2053#[cfg(test)]
2054mod tests {
2055 use cap_std_ext::cap_tempfile;
2056 use oci_image::{DescriptorBuilder, MediaType, Sha256Digest};
2057
2058 use super::*;
2059
2060 #[test]
2061 fn test_ref_for_descriptor() {
2062 let d = DescriptorBuilder::default()
2063 .size(42u64)
2064 .media_type(MediaType::ImageManifest)
2065 .digest(
2066 Sha256Digest::from_str(
2067 "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
2068 )
2069 .unwrap(),
2070 )
2071 .build()
2072 .unwrap();
2073 assert_eq!(ref_for_layer(&d).unwrap(), "ostree/container/blob/sha256_3A_2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae");
2074 }
2075
2076 #[test]
2077 fn test_cleanup_root() -> Result<()> {
2078 let td = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2079 let usretc = "usr/etc";
2080 cleanup_root(&td).unwrap();
2081 td.create_dir_all(usretc)?;
2082 let usretc = &td.open_dir(usretc)?;
2083 usretc.write("hostname", b"hostname")?;
2084 cleanup_root(&td).unwrap();
2085 assert!(usretc.try_exists("hostname")?);
2086 usretc.write("hostname", b"")?;
2087 cleanup_root(&td).unwrap();
2088 assert!(!td.try_exists("hostname")?);
2089
2090 usretc.symlink_contents("../run/systemd/stub-resolv.conf", "resolv.conf")?;
2091 cleanup_root(&td).unwrap();
2092 assert!(usretc.symlink_metadata("resolv.conf")?.is_symlink());
2093 usretc.remove_file("resolv.conf")?;
2094 usretc.write("resolv.conf", b"")?;
2095 cleanup_root(&td).unwrap();
2096 assert!(!usretc.try_exists("resolv.conf")?);
2097
2098 Ok(())
2099 }
2100}