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::{Context, anyhow};
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!(
696 "containers-policy.json specifies a default of `insecureAcceptAnything`; refusing usage"
697 ));
698 }
699 SignatureSource::OstreeRemote(_) if verify_layers => {
700 return Err(anyhow!(
701 "Cannot currently verify layered containers via ostree remote"
702 ));
703 }
704 _ => {}
705 }
706
707 let previous_state = try_query_image(&self.repo, &self.imgref.imgref)?;
709
710 let target_reference = self.imgref.imgref.name.parse::<Reference>().ok();
712 let previous_state = if let Some(target_digest) = target_reference
713 .as_ref()
714 .and_then(|v| v.digest())
715 .map(Digest::from_str)
716 .transpose()?
717 {
718 if let Some(previous_state) = previous_state {
719 if previous_state.manifest_digest == target_digest {
721 tracing::debug!("Digest-based pullspec {:?} already present", self.imgref);
722 return Ok(PrepareResult::AlreadyPresent(previous_state));
723 }
724 Some(previous_state)
725 } else {
726 None
727 }
728 } else {
729 previous_state
730 };
731
732 if self.offline {
733 anyhow::bail!("Manifest fetch required in offline mode");
734 }
735
736 let proxy_img = self
737 .proxy
738 .open_image(&self.imgref.imgref.to_string())
739 .await?;
740
741 let (manifest_digest, manifest) = self.proxy.fetch_manifest(&proxy_img).await?;
742 let manifest_digest = Digest::from_str(&manifest_digest)?;
743 let new_imageid = manifest.config().digest();
744
745 let (previous_state, previous_imageid) = if let Some(previous_state) = previous_state {
748 if previous_state.manifest_digest == manifest_digest {
750 return Ok(PrepareResult::AlreadyPresent(previous_state));
751 }
752 let previous_imageid = previous_state.manifest.config().digest();
754 if previous_imageid == new_imageid {
755 return Ok(PrepareResult::AlreadyPresent(previous_state));
756 }
757 let previous_imageid = previous_imageid.to_string();
758 (Some(previous_state), Some(previous_imageid))
759 } else {
760 (None, None)
761 };
762
763 let config = self.proxy.fetch_config(&proxy_img).await?;
764
765 if let Some(previous_state) = previous_state.as_ref() {
768 self.cache_pending(
769 previous_state.merge_commit.as_str(),
770 &manifest_digest,
771 &manifest,
772 &config,
773 )
774 .await?;
775 }
776
777 let imp = self.create_prepared_import(
778 manifest_digest,
779 manifest,
780 config,
781 previous_state,
782 previous_imageid,
783 proxy_img,
784 )?;
785 Ok(PrepareResult::Ready(imp))
786 }
787
788 #[context("Unencapsulating base")]
790 pub(crate) async fn unencapsulate_base(
791 &self,
792 import: &mut store::PreparedImport,
793 require_ostree: bool,
794 write_refs: bool,
795 ) -> Result<()> {
796 tracing::debug!("Fetching base");
797 if matches!(self.imgref.sigverify, SignatureSource::ContainerPolicy)
798 && skopeo::container_policy_is_default_insecure()?
799 {
800 return Err(anyhow!(
801 "containers-policy.json specifies a default of `insecureAcceptAnything`; refusing usage"
802 ));
803 }
804 let remote = match &self.imgref.sigverify {
805 SignatureSource::OstreeRemote(remote) => Some(remote.clone()),
806 SignatureSource::ContainerPolicy | SignatureSource::ContainerPolicyAllowInsecure => {
807 None
808 }
809 };
810 let Some(commit_layer) = import.ostree_commit_layer.as_mut() else {
811 if require_ostree {
812 anyhow::bail!(
813 "No {DIFFID_LABEL} label found, not an ostree encapsulated container"
814 );
815 }
816 return Ok(());
817 };
818 let des_layers = self.proxy.get_layer_info(&import.proxy_img).await?;
819 for layer in import.ostree_layers.iter_mut() {
820 if layer.commit.is_some() {
821 continue;
822 }
823 if let Some(p) = self.layer_progress.as_ref() {
824 p.send(ImportProgress::OstreeChunkStarted(layer.layer.clone()))
825 .await?;
826 }
827 let (blob, driver, media_type) = fetch_layer(
828 &self.proxy,
829 &import.proxy_img,
830 &import.manifest,
831 &layer.layer,
832 self.layer_byte_progress.as_ref(),
833 des_layers.as_ref(),
834 self.imgref.imgref.transport,
835 )
836 .await?;
837 let repo = self.repo.clone();
838 let target_ref = layer.ostree_ref.clone();
839 let import_task =
840 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
841 let txn = repo.auto_transaction(Some(cancellable))?;
842 let mut importer = crate::tar::Importer::new_for_object_set(&repo);
843 let blob = tokio_util::io::SyncIoBridge::new(blob);
844 let mut blob = Decompressor::new(&media_type, blob)?;
845 let mut archive = tar::Archive::new(&mut blob);
846 importer.import_objects(&mut archive, Some(cancellable))?;
847 let commit = if write_refs {
848 let commit = importer.finish_import_object_set()?;
849 repo.transaction_set_ref(None, &target_ref, Some(commit.as_str()));
850 tracing::debug!("Wrote {} => {}", target_ref, commit);
851 Some(commit)
852 } else {
853 None
854 };
855 txn.commit(Some(cancellable))?;
856 blob.finish()?;
857 Ok::<_, anyhow::Error>(commit)
858 })
859 .map_err(|e| e.context(format!("Layer {}", layer.layer.digest())));
860 let commit = super::unencapsulate::join_fetch(import_task, driver).await?;
861 layer.commit = commit;
862 if let Some(p) = self.layer_progress.as_ref() {
863 p.send(ImportProgress::OstreeChunkCompleted(layer.layer.clone()))
864 .await?;
865 }
866 }
867 if commit_layer.commit.is_none() {
868 if let Some(p) = self.layer_progress.as_ref() {
869 p.send(ImportProgress::OstreeChunkStarted(
870 commit_layer.layer.clone(),
871 ))
872 .await?;
873 }
874 let (blob, driver, media_type) = fetch_layer(
875 &self.proxy,
876 &import.proxy_img,
877 &import.manifest,
878 &commit_layer.layer,
879 self.layer_byte_progress.as_ref(),
880 des_layers.as_ref(),
881 self.imgref.imgref.transport,
882 )
883 .await?;
884 let repo = self.repo.clone();
885 let target_ref = commit_layer.ostree_ref.clone();
886 let import_task =
887 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
888 let txn = repo.auto_transaction(Some(cancellable))?;
889 let mut importer = crate::tar::Importer::new_for_commit(&repo, remote);
890 let blob = tokio_util::io::SyncIoBridge::new(blob);
891 let mut blob = Decompressor::new(&media_type, blob)?;
892 let mut archive = tar::Archive::new(&mut blob);
893 importer.import_commit(&mut archive, Some(cancellable))?;
894 let (commit, verify_text) = importer.finish_import_commit();
895 if write_refs {
896 repo.transaction_set_ref(None, &target_ref, Some(commit.as_str()));
897 tracing::debug!("Wrote {} => {}", target_ref, commit);
898 }
899 repo.mark_commit_partial(&commit, false)?;
900 txn.commit(Some(cancellable))?;
901 blob.finish()?;
902 Ok::<_, anyhow::Error>((commit, verify_text))
903 });
904 let (commit, verify_text) =
905 super::unencapsulate::join_fetch(import_task, driver).await?;
906 commit_layer.commit = Some(commit);
907 import.verify_text = verify_text;
908 if let Some(p) = self.layer_progress.as_ref() {
909 p.send(ImportProgress::OstreeChunkCompleted(
910 commit_layer.layer.clone(),
911 ))
912 .await?;
913 }
914 };
915 Ok(())
916 }
917
918 pub async fn unencapsulate(mut self) -> Result<Import> {
923 let mut prep = match self.prepare_internal(false).await? {
924 PrepareResult::AlreadyPresent(_) => {
925 panic!("Should not have image present for unencapsulation")
926 }
927 PrepareResult::Ready(r) => r,
928 };
929 if !prep.layers.is_empty() {
930 anyhow::bail!("Image has {} non-ostree layers", prep.layers.len());
931 }
932 let deprecated_warning = prep.deprecated_warning().map(ToOwned::to_owned);
933 self.unencapsulate_base(&mut prep, true, false).await?;
934 self.proxy.close_image(&prep.proxy_img).await?;
937 let ostree_commit = prep.ostree_commit_layer.unwrap().commit.unwrap();
939 let image_digest = prep.manifest_digest;
940 Ok(Import {
941 ostree_commit,
942 image_digest,
943 deprecated_warning,
944 })
945 }
946
947 fn write_merge_commit_impl(
950 repo: &ostree::Repo,
951 base_commit: Option<&str>,
952 layer_commits: &[String],
953 have_derived_layers: bool,
954 metadata: glib::Variant,
955 timestamp: u64,
956 ostree_ref: &str,
957 no_imgref: bool,
958 disable_gc: bool,
959 cancellable: Option<&gio::Cancellable>,
960 ) -> Result<Box<LayeredImageState>> {
961 use rustix::fd::AsRawFd;
962
963 let txn = repo.auto_transaction(cancellable)?;
964
965 let devino = ostree::RepoDevInoCache::new();
966 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
967 let repo_tmp = repodir.open_dir("tmp")?;
968 let td = cap_std_ext::cap_tempfile::TempDir::new_in(&repo_tmp)?;
969
970 let rootpath = "root";
971 let checkout_mode = if repo.mode() == ostree::RepoMode::Bare {
972 ostree::RepoCheckoutMode::None
973 } else {
974 ostree::RepoCheckoutMode::User
975 };
976 let mut checkout_opts = ostree::RepoCheckoutAtOptions {
977 mode: checkout_mode,
978 overwrite_mode: ostree::RepoCheckoutOverwriteMode::UnionFiles,
979 devino_to_csum_cache: Some(devino.clone()),
980 no_copy_fallback: true,
981 force_copy_zerosized: true,
982 process_whiteouts: false,
983 ..Default::default()
984 };
985 if let Some(base) = base_commit.as_ref() {
986 repo.checkout_at(
987 Some(&checkout_opts),
988 (*td).as_raw_fd(),
989 rootpath,
990 &base,
991 cancellable,
992 )
993 .context("Checking out base commit")?;
994 }
995
996 checkout_opts.process_whiteouts = true;
998 for commit in layer_commits {
999 tracing::debug!("Unpacking {commit}");
1000 repo.checkout_at(
1001 Some(&checkout_opts),
1002 (*td).as_raw_fd(),
1003 rootpath,
1004 &commit,
1005 cancellable,
1006 )
1007 .with_context(|| format!("Checking out layer {commit}"))?;
1008 }
1009
1010 let root_dir = td.open_dir(rootpath)?;
1011
1012 let modifier =
1013 ostree::RepoCommitModifier::new(ostree::RepoCommitModifierFlags::empty(), None);
1014 modifier.set_devino_cache(&devino);
1015 if have_derived_layers {
1019 let sepolicy = ostree::SePolicy::new_at(root_dir.as_raw_fd(), cancellable)?;
1020 tracing::debug!("labeling from merged tree");
1021 modifier.set_sepolicy(Some(&sepolicy));
1022 } else if let Some(base) = base_commit.as_ref() {
1023 tracing::debug!("labeling from base tree");
1024 modifier.set_sepolicy_from_commit(repo, &base, cancellable)?;
1026 } else {
1027 panic!("Unexpected state: no derived layers and no base")
1028 }
1029
1030 cleanup_root(&root_dir)?;
1031
1032 let mt = ostree::MutableTree::new();
1033 repo.write_dfd_to_mtree(
1034 (*td).as_raw_fd(),
1035 rootpath,
1036 &mt,
1037 Some(&modifier),
1038 cancellable,
1039 )
1040 .context("Writing merged filesystem to mtree")?;
1041
1042 let merged_root = repo
1043 .write_mtree(&mt, cancellable)
1044 .context("Writing mtree")?;
1045 let merged_root = merged_root.downcast::<ostree::RepoFile>().unwrap();
1046 let parent = base_commit.as_deref();
1049 let merged_commit = repo
1050 .write_commit_with_time(
1051 parent,
1052 None,
1053 None,
1054 Some(&metadata),
1055 &merged_root,
1056 timestamp,
1057 cancellable,
1058 )
1059 .context("Writing commit")?;
1060 if !no_imgref {
1061 repo.transaction_set_ref(None, ostree_ref, Some(merged_commit.as_str()));
1062 }
1063 txn.commit(cancellable)?;
1064
1065 if !disable_gc {
1066 let n: u32 = gc_image_layers_impl(repo, cancellable)?;
1067 tracing::debug!("pruned {n} layers");
1068 }
1069
1070 let state = query_image_commit(repo, &merged_commit)?;
1073 Ok(state)
1074 }
1075
1076 #[context("Importing")]
1080 pub async fn import(
1081 mut self,
1082 mut import: Box<PreparedImport>,
1083 ) -> Result<Box<LayeredImageState>> {
1084 if let Some(status) = import.format_layer_status() {
1085 system_repo_journal_print(&self.repo, libsystemd::logging::Priority::Info, &status);
1086 }
1087 self.unencapsulate_base(&mut import, false, true).await?;
1090 let des_layers = self.proxy.get_layer_info(&import.proxy_img).await?;
1091 let proxy = self.proxy;
1092 let target_imgref = self.target_imgref.as_ref().unwrap_or(&self.imgref);
1093 let base_commit = import
1094 .ostree_commit_layer
1095 .as_ref()
1096 .map(|c| c.commit.clone().unwrap());
1097
1098 let root_is_transient = if let Some(base) = base_commit.as_ref() {
1099 let rootf = self.repo.read_commit(&base, gio::Cancellable::NONE)?.0;
1100 let rootf = rootf.downcast_ref::<ostree::RepoFile>().unwrap();
1101 crate::ostree_prepareroot::overlayfs_root_enabled(rootf)?
1102 } else {
1103 true
1105 };
1106 tracing::debug!("Base rootfs is transient: {root_is_transient}");
1107
1108 let ostree_ref = ref_for_image(&target_imgref.imgref)?;
1109
1110 let mut layer_commits = Vec::new();
1111 let mut layer_filtered_content: Option<MetaFilteredData> = None;
1112 let have_derived_layers = !import.layers.is_empty();
1113 tracing::debug!("Processing layers: {}", import.layers.len());
1114 for layer in import.layers {
1115 if let Some(c) = layer.commit {
1116 tracing::debug!("Reusing fetched commit {}", c);
1117 layer_commits.push(c.to_string());
1118 } else {
1119 if let Some(p) = self.layer_progress.as_ref() {
1120 p.send(ImportProgress::DerivedLayerStarted(layer.layer.clone()))
1121 .await?;
1122 }
1123 let (blob, driver, media_type) = super::unencapsulate::fetch_layer(
1124 &proxy,
1125 &import.proxy_img,
1126 &import.manifest,
1127 &layer.layer,
1128 self.layer_byte_progress.as_ref(),
1129 des_layers.as_ref(),
1130 self.imgref.imgref.transport,
1131 )
1132 .await?;
1133 let opts = crate::tar::WriteTarOptions {
1136 base: base_commit.clone(),
1137 selinux: true,
1138 allow_nonusr: root_is_transient,
1139 retain_var: self.ostree_v2024_3,
1140 };
1141 let r = crate::tar::write_tar(
1142 &self.repo,
1143 blob,
1144 media_type,
1145 layer.ostree_ref.as_str(),
1146 Some(opts),
1147 );
1148 let r = super::unencapsulate::join_fetch(r, driver)
1149 .await
1150 .with_context(|| format!("Parsing layer blob {}", layer.layer.digest()))?;
1151 tracing::debug!("Imported layer: {}", r.commit.as_str());
1152 layer_commits.push(r.commit);
1153 let filtered_owned = HashMap::from_iter(r.filtered.clone());
1154 if let Some((filtered, n_rest)) = bootc_utils::collect_until(
1155 r.filtered.iter(),
1156 const { NonZeroUsize::new(5).unwrap() },
1157 ) {
1158 let mut msg = String::new();
1159 for (path, n) in filtered {
1160 write!(msg, "{path}: {n} ").unwrap();
1161 }
1162 if n_rest > 0 {
1163 write!(msg, "...and {n_rest} more").unwrap();
1164 }
1165 tracing::debug!("Found filtered toplevels: {msg}");
1166 layer_filtered_content
1167 .get_or_insert_default()
1168 .insert(layer.layer.digest().to_string(), filtered_owned);
1169 } else {
1170 tracing::debug!("No filtered content");
1171 }
1172 if let Some(p) = self.layer_progress.as_ref() {
1173 p.send(ImportProgress::DerivedLayerCompleted(layer.layer.clone()))
1174 .await?;
1175 }
1176 }
1177 }
1178
1179 proxy.close_image(&import.proxy_img).await?;
1182
1183 proxy.finalize().await?;
1185 tracing::debug!("finalized proxy");
1186
1187 let _ = self.layer_byte_progress.take();
1189 let _ = self.layer_progress.take();
1190
1191 let mut metadata = BTreeMap::new();
1192 metadata.insert(
1193 META_MANIFEST_DIGEST,
1194 import.manifest_digest.to_string().to_variant(),
1195 );
1196 metadata.insert(
1197 META_MANIFEST,
1198 import.manifest.to_canon_json_string()?.to_variant(),
1199 );
1200 metadata.insert(
1201 META_CONFIG,
1202 import.config.to_canon_json_string()?.to_variant(),
1203 );
1204 metadata.insert(
1205 "ostree.importer.version",
1206 env!("CARGO_PKG_VERSION").to_variant(),
1207 );
1208 let metadata = metadata.to_variant();
1209
1210 let timestamp = timestamp_of_manifest_or_config(&import.manifest, &import.config)
1211 .unwrap_or_else(|| chrono::offset::Utc::now().timestamp() as u64);
1212 let repo = self.repo;
1214 let mut state = crate::tokio_util::spawn_blocking_cancellable_flatten(
1215 move |cancellable| -> Result<Box<LayeredImageState>> {
1216 Self::write_merge_commit_impl(
1217 &repo,
1218 base_commit.as_deref(),
1219 &layer_commits,
1220 have_derived_layers,
1221 metadata,
1222 timestamp,
1223 &ostree_ref,
1224 self.no_imgref,
1225 self.disable_gc,
1226 Some(cancellable),
1227 )
1228 },
1229 )
1230 .await?;
1231 state.verify_text = import.verify_text;
1233 state.filtered_files = layer_filtered_content;
1234 Ok(state)
1235 }
1236}
1237
1238pub fn list_images(repo: &ostree::Repo) -> Result<Vec<String>> {
1240 let cancellable = gio::Cancellable::NONE;
1241 let refs = repo.list_refs_ext(
1242 Some(IMAGE_PREFIX),
1243 ostree::RepoListRefsExtFlags::empty(),
1244 cancellable,
1245 )?;
1246 refs.keys()
1247 .map(|imgname| refescape::unprefix_unescape_ref(IMAGE_PREFIX, imgname))
1248 .collect()
1249}
1250
1251fn try_query_image(
1254 repo: &ostree::Repo,
1255 imgref: &ImageReference,
1256) -> Result<Option<Box<LayeredImageState>>> {
1257 let ostree_ref = &ref_for_image(imgref)?;
1258 if let Some(merge_rev) = repo.resolve_rev(ostree_ref, true)? {
1259 match query_image_commit(repo, merge_rev.as_str()) {
1260 Ok(r) => Ok(Some(r)),
1261 Err(e) => {
1262 eprintln!("error: failed to query image commit: {e}");
1263 Ok(None)
1264 }
1265 }
1266 } else {
1267 Ok(None)
1268 }
1269}
1270
1271#[context("Querying image {imgref}")]
1273pub fn query_image(
1274 repo: &ostree::Repo,
1275 imgref: &ImageReference,
1276) -> Result<Option<Box<LayeredImageState>>> {
1277 let ostree_ref = &ref_for_image(imgref)?;
1278 let merge_rev = repo.resolve_rev(ostree_ref, true)?;
1279 merge_rev
1280 .map(|r| query_image_commit(repo, r.as_str()))
1281 .transpose()
1282}
1283
1284fn parse_cached_update(meta: &glib::VariantDict) -> Result<Option<CachedImageUpdate>> {
1286 let manifest_digest =
1288 if let Some(d) = meta.lookup::<String>(ImageImporter::CACHED_KEY_MANIFEST_DIGEST)? {
1289 d
1290 } else {
1291 return Ok(None);
1294 };
1295 let manifest_digest = Digest::from_str(&manifest_digest)?;
1296 let manifest = meta.lookup_value(ImageImporter::CACHED_KEY_MANIFEST, None);
1299 let manifest: oci_image::ImageManifest = manifest
1300 .as_ref()
1301 .and_then(|v| v.str())
1302 .map(serde_json::from_str)
1303 .transpose()?
1304 .ok_or_else(|| {
1305 anyhow!(
1306 "Expected cached manifest {}",
1307 ImageImporter::CACHED_KEY_MANIFEST
1308 )
1309 })?;
1310 let config = meta.lookup_value(ImageImporter::CACHED_KEY_CONFIG, None);
1311 let config: oci_image::ImageConfiguration = config
1312 .as_ref()
1313 .and_then(|v| v.str())
1314 .map(serde_json::from_str)
1315 .transpose()?
1316 .ok_or_else(|| {
1317 anyhow!(
1318 "Expected cached manifest {}",
1319 ImageImporter::CACHED_KEY_CONFIG
1320 )
1321 })?;
1322 Ok(Some(CachedImageUpdate {
1323 manifest,
1324 config,
1325 manifest_digest,
1326 }))
1327}
1328
1329#[context("Clearing cached update {imgref}")]
1331pub fn clear_cached_update(repo: &ostree::Repo, imgref: &ImageReference) -> Result<()> {
1332 let cancellable = gio::Cancellable::NONE;
1333 let ostree_ref = ref_for_image(imgref)?;
1334 let rev = repo.require_rev(&ostree_ref)?;
1335 let Some(commitmeta) = repo.read_commit_detached_metadata(&rev, cancellable)? else {
1336 return Ok(());
1337 };
1338
1339 let mut commitmeta: BTreeMap<String, glib::Variant> =
1341 BTreeMap::from_variant(&commitmeta).unwrap();
1342 let mut changed = false;
1343 for key in [
1344 ImageImporter::CACHED_KEY_CONFIG,
1345 ImageImporter::CACHED_KEY_MANIFEST,
1346 ImageImporter::CACHED_KEY_MANIFEST_DIGEST,
1347 ] {
1348 if commitmeta.remove(key).is_some() {
1349 changed = true;
1350 }
1351 }
1352 if !changed {
1353 return Ok(());
1354 }
1355 let commitmeta = glib::Variant::from(commitmeta);
1356 repo.write_commit_detached_metadata(&rev, Some(&commitmeta), cancellable)?;
1357 Ok(())
1358}
1359
1360pub fn query_image_commit(repo: &ostree::Repo, commit: &str) -> Result<Box<LayeredImageState>> {
1363 let merge_commit = commit.to_string();
1364 let merge_commit_obj = repo.load_commit(commit)?.0;
1365 let commit_meta = &merge_commit_obj.child_value(0);
1366 let commit_meta = &ostree::glib::VariantDict::new(Some(commit_meta));
1367 let (manifest, manifest_digest) = manifest_data_from_commitmeta(commit_meta)?;
1368 let configuration = image_config_from_commitmeta(commit_meta)?;
1369 let mut layers = manifest.layers().iter().cloned();
1370 let base_layer = layers.next().ok_or_else(|| anyhow!("No layers found"))?;
1372 let base_layer = query_layer(repo, base_layer)?;
1373 let ostree_ref = base_layer.ostree_ref.as_str();
1374 let base_commit = base_layer
1375 .commit
1376 .ok_or_else(|| anyhow!("Missing base image ref {ostree_ref}"))?;
1377
1378 let detached_commitmeta =
1379 repo.read_commit_detached_metadata(&merge_commit, gio::Cancellable::NONE)?;
1380 let detached_commitmeta = detached_commitmeta
1381 .as_ref()
1382 .map(|v| glib::VariantDict::new(Some(v)));
1383 let cached_update = detached_commitmeta
1384 .as_ref()
1385 .map(parse_cached_update)
1386 .transpose()?
1387 .flatten();
1388 let state = Box::new(LayeredImageState {
1389 base_commit,
1390 merge_commit,
1391 manifest_digest,
1392 manifest,
1393 configuration,
1394 cached_update,
1395 verify_text: None,
1397 filtered_files: None,
1398 });
1399 tracing::debug!("Wrote merge commit {}", state.merge_commit);
1400 Ok(state)
1401}
1402
1403fn manifest_for_image(repo: &ostree::Repo, imgref: &ImageReference) -> Result<ImageManifest> {
1404 let ostree_ref = ref_for_image(imgref)?;
1405 let rev = repo.require_rev(&ostree_ref)?;
1406 let (commit_obj, _) = repo.load_commit(rev.as_str())?;
1407 let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
1408 Ok(manifest_data_from_commitmeta(commit_meta)?.0)
1409}
1410
1411#[context("Copying image")]
1414pub async fn copy(
1415 src_repo: &ostree::Repo,
1416 src_imgref: &ImageReference,
1417 dest_repo: &ostree::Repo,
1418 dest_imgref: &ImageReference,
1419) -> Result<()> {
1420 let src_ostree_ref = ref_for_image(src_imgref)?;
1421 let src_commit = src_repo.require_rev(&src_ostree_ref)?;
1422 let manifest = manifest_for_image(src_repo, src_imgref)?;
1423 let layer_refs = manifest
1425 .layers()
1426 .iter()
1427 .map(ref_for_layer)
1428 .chain(std::iter::once(Ok(src_commit.to_string())));
1429 for ostree_ref in layer_refs {
1430 let ostree_ref = ostree_ref?;
1431 let src_repo = src_repo.clone();
1432 let dest_repo = dest_repo.clone();
1433 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| -> Result<_> {
1434 let cancellable = Some(cancellable);
1435 let srcfd = &format!("file:///proc/self/fd/{}", src_repo.dfd());
1436 let flags = ostree::RepoPullFlags::MIRROR;
1437 let opts = glib::VariantDict::new(None);
1438 let refs = [ostree_ref.as_str()];
1439 opts.insert("disable-verify-bindings", true);
1441 opts.insert("refs", &refs[..]);
1442 opts.insert("flags", flags.bits() as i32);
1443 let options = opts.to_variant();
1444 dest_repo.pull_with_options(srcfd, &options, None, cancellable)?;
1445 Ok(())
1446 })
1447 .await?;
1448 }
1449
1450 let dest_ostree_ref = ref_for_image(dest_imgref)?;
1451 dest_repo.set_ref_immediate(
1452 None,
1453 &dest_ostree_ref,
1454 Some(&src_commit),
1455 gio::Cancellable::NONE,
1456 )?;
1457
1458 Ok(())
1459}
1460
1461#[derive(Clone, Debug, Default)]
1463#[non_exhaustive]
1464pub struct ExportToOCIOpts {
1465 pub skip_compression: bool,
1467 pub authfile: Option<std::path::PathBuf>,
1469 pub progress_to_stdout: bool,
1471}
1472
1473fn chunking_from_layer_committed(
1478 repo: &ostree::Repo,
1479 l: &Descriptor,
1480 chunking: &mut chunking::Chunking,
1481) -> Result<()> {
1482 let mut chunk = Chunk::default();
1483 let layer_ref = &ref_for_layer(l)?;
1484 let root = repo.read_commit(layer_ref, gio::Cancellable::NONE)?.0;
1485 let e = root.enumerate_children(
1486 "standard::name,standard::size",
1487 gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS,
1488 gio::Cancellable::NONE,
1489 )?;
1490 for child in e.clone() {
1491 let child = &child?;
1492 let name = child.name();
1494 let name = Utf8Path::from_path(&name).unwrap();
1496 ostree::validate_checksum_string(name.as_str())?;
1497 chunking.remainder.move_obj(&mut chunk, name.as_str());
1498 }
1499 chunking.chunks.push(chunk);
1500 Ok(())
1501}
1502
1503#[context("Copying image")]
1505pub(crate) fn export_to_oci(
1506 repo: &ostree::Repo,
1507 imgref: &ImageReference,
1508 dest_oci: &Dir,
1509 tag: Option<&str>,
1510 opts: ExportToOCIOpts,
1511) -> Result<Descriptor> {
1512 let srcinfo = query_image(repo, imgref)?.ok_or_else(|| anyhow!("No such image"))?;
1513 let (commit_layer, component_layers, remaining_layers) =
1514 parse_manifest_layout(&srcinfo.manifest, &srcinfo.configuration)?;
1515
1516 let mut new_manifest = srcinfo.manifest.clone();
1520 new_manifest.layers_mut().clear();
1521 let mut new_config = srcinfo.configuration.clone();
1522 if let Some(history) = new_config.history_mut() {
1523 history.clear();
1524 }
1525 new_config.rootfs_mut().diff_ids_mut().clear();
1526
1527 let opts = ExportOpts {
1528 skip_compression: opts.skip_compression,
1529 authfile: opts.authfile,
1530 ..Default::default()
1531 };
1532
1533 let mut labels = HashMap::new();
1534
1535 let mut dest_oci = ocidir::OciDir::ensure(dest_oci.try_clone()?)?;
1536
1537 let commit_chunk_ref = commit_layer
1538 .as_ref()
1539 .map(|l| ref_for_layer(l))
1540 .transpose()?;
1541 let commit_chunk_rev = commit_chunk_ref
1542 .as_ref()
1543 .map(|r| repo.require_rev(&r))
1544 .transpose()?;
1545 if let Some(commit_chunk_rev) = commit_chunk_rev {
1546 let mut chunking = chunking::Chunking::new(repo, &commit_chunk_rev)?;
1547 for layer in component_layers {
1548 chunking_from_layer_committed(repo, layer, &mut chunking)?;
1549 }
1550
1551 export_chunked(
1554 repo,
1555 &srcinfo.base_commit,
1556 &mut dest_oci,
1557 &mut new_manifest,
1558 &mut new_config,
1559 &mut labels,
1560 chunking,
1561 &opts,
1562 "",
1563 )?;
1564 }
1565
1566 let compression = opts.skip_compression.then_some(Compression::none());
1568 for (i, layer) in remaining_layers.iter().enumerate() {
1569 let layer_ref = &ref_for_layer(layer)?;
1570 let mut target_blob = dest_oci.create_gzip_layer(compression)?;
1571 let export_opts = crate::tar::ExportOptions { raw: true };
1573 crate::tar::export_commit(
1574 repo,
1575 layer_ref.as_str(),
1576 &mut target_blob,
1577 Some(export_opts),
1578 )?;
1579 let layer = target_blob.complete()?;
1580 let previous_annotations = srcinfo
1581 .manifest
1582 .layers()
1583 .get(i)
1584 .and_then(|l| l.annotations().as_ref())
1585 .cloned();
1586 let history = srcinfo.configuration.history().as_ref();
1587 let history_entry = history.and_then(|v| v.get(i));
1588 let previous_description = history_entry
1589 .clone()
1590 .and_then(|h| h.comment().as_deref())
1591 .unwrap_or_default();
1592
1593 let previous_created = history_entry
1594 .and_then(|h| h.created().as_deref())
1595 .and_then(bootc_utils::try_deserialize_timestamp)
1596 .unwrap_or_default();
1597
1598 dest_oci.push_layer_full(
1599 &mut new_manifest,
1600 &mut new_config,
1601 layer,
1602 previous_annotations,
1603 previous_description,
1604 previous_created,
1605 )
1606 }
1607
1608 let new_config = dest_oci.write_config(new_config)?;
1609 new_manifest.set_config(new_config);
1610
1611 Ok(dest_oci.insert_manifest(new_manifest, tag, oci_image::Platform::default())?)
1612}
1613
1614#[context("Export")]
1617pub async fn export(
1618 repo: &ostree::Repo,
1619 src_imgref: &ImageReference,
1620 dest_imgref: &ImageReference,
1621 opts: Option<ExportToOCIOpts>,
1622) -> Result<oci_image::Digest> {
1623 let opts = opts.unwrap_or_default();
1624 let target_oci = dest_imgref.transport == Transport::OciDir;
1625 let tempdir = if !target_oci {
1626 let vartmp = cap_std::fs::Dir::open_ambient_dir("/var/tmp", cap_std::ambient_authority())?;
1627 let td = cap_std_ext::cap_tempfile::TempDir::new_in(&vartmp)?;
1628 let opts = ExportToOCIOpts {
1630 skip_compression: true,
1631 progress_to_stdout: opts.progress_to_stdout,
1632 ..Default::default()
1633 };
1634 export_to_oci(repo, src_imgref, &td, None, opts)?;
1635 td
1636 } else {
1637 let (path, tag) = parse_oci_path_and_tag(dest_imgref.name.as_str());
1638 tracing::debug!("using OCI path={path} tag={tag:?}");
1639 let path = Dir::open_ambient_dir(path, cap_std::ambient_authority())
1640 .with_context(|| format!("Opening {path}"))?;
1641 let descriptor = export_to_oci(repo, src_imgref, &path, tag, opts)?;
1642 return Ok(descriptor.digest().clone());
1643 };
1644 let target_fd = 3i32;
1646 let tempoci = ImageReference {
1647 transport: Transport::OciDir,
1648 name: format!("/proc/self/fd/{target_fd}"),
1649 };
1650 let authfile = opts.authfile.as_deref();
1651 skopeo::copy(
1652 &tempoci,
1653 dest_imgref,
1654 authfile,
1655 Some((std::sync::Arc::new(tempdir.try_clone()?.into()), target_fd)),
1656 opts.progress_to_stdout,
1657 )
1658 .await
1659}
1660
1661#[context("Listing deployment manifests")]
1664fn list_container_deployment_manifests(
1665 repo: &ostree::Repo,
1666 cancellable: Option<&gio::Cancellable>,
1667) -> Result<Vec<ImageManifest>> {
1668 let commits = OSTREE_BASE_DEPLOYMENT_REFS
1671 .iter()
1672 .chain(RPMOSTREE_BASE_REFS)
1673 .chain(std::iter::once(&BASE_IMAGE_PREFIX))
1674 .try_fold(
1675 std::collections::HashSet::new(),
1676 |mut acc, &p| -> Result<_> {
1677 let refs = repo.list_refs_ext(
1678 Some(p),
1679 ostree::RepoListRefsExtFlags::empty(),
1680 cancellable,
1681 )?;
1682 for (_, v) in refs {
1683 acc.insert(v);
1684 }
1685 Ok(acc)
1686 },
1687 )?;
1688 let mut r = Vec::new();
1690 for commit in commits {
1691 let commit_obj = repo.load_commit(&commit)?.0;
1692 let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
1693 if commit_meta
1694 .lookup::<String>(META_MANIFEST_DIGEST)?
1695 .is_some()
1696 {
1697 tracing::trace!("Commit {commit} is a container image");
1698 let manifest = manifest_data_from_commitmeta(commit_meta)?.0;
1699 r.push(manifest);
1700 }
1701 }
1702 Ok(r)
1703}
1704
1705pub fn gc_image_layers(repo: &ostree::Repo) -> Result<u32> {
1711 gc_image_layers_impl(repo, gio::Cancellable::NONE)
1712}
1713
1714#[context("Pruning image layers")]
1715fn gc_image_layers_impl(
1716 repo: &ostree::Repo,
1717 cancellable: Option<&gio::Cancellable>,
1718) -> Result<u32> {
1719 let all_images = list_images(repo)?;
1720 let deployment_commits = list_container_deployment_manifests(repo, cancellable)?;
1721 let all_manifests = all_images
1722 .into_iter()
1723 .map(|img| {
1724 ImageReference::try_from(img.as_str()).and_then(|ir| manifest_for_image(repo, &ir))
1725 })
1726 .chain(deployment_commits.into_iter().map(Ok))
1727 .collect::<Result<Vec<_>>>()?;
1728 tracing::debug!("Images found: {}", all_manifests.len());
1729 let mut referenced_layers = BTreeSet::new();
1730 for m in all_manifests.iter() {
1731 for layer in m.layers() {
1732 referenced_layers.insert(layer.digest().to_string());
1733 }
1734 }
1735 tracing::debug!("Referenced layers: {}", referenced_layers.len());
1736 let found_layers = repo
1737 .list_refs_ext(
1738 Some(LAYER_PREFIX),
1739 ostree::RepoListRefsExtFlags::empty(),
1740 cancellable,
1741 )?
1742 .into_iter()
1743 .map(|v| v.0);
1744 tracing::debug!("Found layers: {}", found_layers.len());
1745 let mut pruned = 0u32;
1746 for layer_ref in found_layers {
1747 let layer_digest = refescape::unprefix_unescape_ref(LAYER_PREFIX, &layer_ref)?;
1748 if referenced_layers.remove(layer_digest.as_str()) {
1749 continue;
1750 }
1751 pruned += 1;
1752 tracing::debug!("Pruning: {}", layer_ref.as_str());
1753 repo.set_ref_immediate(None, layer_ref.as_str(), None, cancellable)?;
1754 }
1755
1756 Ok(pruned)
1757}
1758
1759#[cfg(feature = "internal-testing-api")]
1760pub fn count_layer_references(repo: &ostree::Repo) -> Result<u32> {
1762 let cancellable = gio::Cancellable::NONE;
1763 let n = repo
1764 .list_refs_ext(
1765 Some(LAYER_PREFIX),
1766 ostree::RepoListRefsExtFlags::empty(),
1767 cancellable,
1768 )?
1769 .len();
1770 Ok(n as u32)
1771}
1772
1773pub fn image_filtered_content_warning(
1775 filtered_files: &Option<MetaFilteredData>,
1776) -> Result<Option<String>> {
1777 use std::fmt::Write;
1778
1779 let r = filtered_files.as_ref().map(|v| {
1780 let mut filtered = BTreeMap::<&String, u32>::new();
1781 for paths in v.values() {
1782 for (k, v) in paths {
1783 let e = filtered.entry(k).or_default();
1784 *e += v;
1785 }
1786 }
1787 let mut buf = "Image contains non-ostree compatible file paths:".to_string();
1788 for (k, v) in filtered {
1789 write!(buf, " {k}: {v}").unwrap();
1790 }
1791 buf
1792 });
1793 Ok(r)
1794}
1795
1796#[context("Pruning {img}")]
1803pub fn remove_image(repo: &ostree::Repo, img: &ImageReference) -> Result<bool> {
1804 let ostree_ref = &ref_for_image(img)?;
1805 let found = repo.resolve_rev(ostree_ref, true)?.is_some();
1806 if found {
1809 repo.set_ref_immediate(None, ostree_ref, None, gio::Cancellable::NONE)?;
1810 }
1811 Ok(found)
1812}
1813
1814pub fn remove_images<'a>(
1821 repo: &ostree::Repo,
1822 imgs: impl IntoIterator<Item = &'a ImageReference>,
1823) -> Result<()> {
1824 let mut missing = Vec::new();
1825 for img in imgs.into_iter() {
1826 let found = remove_image(repo, img)?;
1827 if !found {
1828 missing.push(img);
1829 }
1830 }
1831 if !missing.is_empty() {
1832 let missing = missing.into_iter().fold("".to_string(), |mut a, v| {
1833 a.push_str(&v.to_string());
1834 a
1835 });
1836 return Err(anyhow::anyhow!("Missing images: {missing}"));
1837 }
1838 Ok(())
1839}
1840
1841#[derive(Debug, Default)]
1842struct CompareState {
1843 verified: BTreeSet<Utf8PathBuf>,
1844 inode_corrupted: BTreeSet<Utf8PathBuf>,
1845 unknown_corrupted: BTreeSet<Utf8PathBuf>,
1846}
1847
1848impl CompareState {
1849 fn is_ok(&self) -> bool {
1850 self.inode_corrupted.is_empty() && self.unknown_corrupted.is_empty()
1851 }
1852}
1853
1854fn compare_file_info(src: &gio::FileInfo, target: &gio::FileInfo) -> bool {
1855 if src.file_type() != target.file_type() {
1856 return false;
1857 }
1858 if src.size() != target.size() {
1859 return false;
1860 }
1861 for attr in ["unix::uid", "unix::gid", "unix::mode"] {
1862 if src.attribute_uint32(attr) != target.attribute_uint32(attr) {
1863 return false;
1864 }
1865 }
1866 true
1867}
1868
1869#[context("Querying object inode")]
1870fn inode_of_object(repo: &ostree::Repo, checksum: &str) -> Result<u64> {
1871 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
1872 let (prefix, suffix) = checksum.split_at(2);
1873 let objpath = format!("objects/{prefix}/{suffix}.file");
1874 let metadata = repodir.symlink_metadata(objpath)?;
1875 Ok(metadata.ino())
1876}
1877
1878fn compare_commit_trees(
1879 repo: &ostree::Repo,
1880 root: &Utf8Path,
1881 target: &ostree::RepoFile,
1882 expected: &ostree::RepoFile,
1883 exact: bool,
1884 colliding_inodes: &BTreeSet<u64>,
1885 state: &mut CompareState,
1886) -> Result<()> {
1887 let cancellable = gio::Cancellable::NONE;
1888 let queryattrs = "standard::name,standard::type";
1889 let queryflags = gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS;
1890 let expected_iter = expected.enumerate_children(queryattrs, queryflags, cancellable)?;
1891
1892 while let Some(expected_info) = expected_iter.next_file(cancellable)? {
1893 let expected_child = expected_iter.child(&expected_info);
1894 let name = expected_info.name();
1895 let name = name.to_str().expect("UTF-8 ostree name");
1896 let path = Utf8PathBuf::from(format!("{root}{name}"));
1897 let target_child = target.child(name);
1898 let target_info = crate::diff::query_info_optional(&target_child, queryattrs, queryflags)
1899 .context("querying optional to")?;
1900 let is_dir = matches!(expected_info.file_type(), gio::FileType::Directory);
1901 if let Some(target_info) = target_info {
1902 let to_child = target_child
1903 .downcast::<ostree::RepoFile>()
1904 .expect("downcast");
1905 to_child.ensure_resolved()?;
1906 let from_child = expected_child
1907 .downcast::<ostree::RepoFile>()
1908 .expect("downcast");
1909 from_child.ensure_resolved()?;
1910
1911 if is_dir {
1912 let from_contents_checksum = from_child.tree_get_contents_checksum();
1913 let to_contents_checksum = to_child.tree_get_contents_checksum();
1914 if from_contents_checksum != to_contents_checksum {
1915 let subpath = Utf8PathBuf::from(format!("{path}/"));
1916 compare_commit_trees(
1917 repo,
1918 &subpath,
1919 &from_child,
1920 &to_child,
1921 exact,
1922 colliding_inodes,
1923 state,
1924 )?;
1925 }
1926 } else {
1927 let from_checksum = from_child.checksum();
1928 let to_checksum = to_child.checksum();
1929 let matches = if exact {
1930 from_checksum == to_checksum
1931 } else {
1932 compare_file_info(&target_info, &expected_info)
1933 };
1934 if !matches {
1935 let from_inode = inode_of_object(repo, &from_checksum)?;
1936 let to_inode = inode_of_object(repo, &to_checksum)?;
1937 if colliding_inodes.contains(&from_inode)
1938 || colliding_inodes.contains(&to_inode)
1939 {
1940 state.inode_corrupted.insert(path);
1941 } else {
1942 state.unknown_corrupted.insert(path);
1943 }
1944 } else {
1945 state.verified.insert(path);
1946 }
1947 }
1948 } else {
1949 eprintln!("Missing {path}");
1950 state.unknown_corrupted.insert(path);
1951 }
1952 }
1953 Ok(())
1954}
1955
1956#[context("Verifying container image state")]
1957pub(crate) fn verify_container_image(
1958 sysroot: &SysrootLock,
1959 imgref: &ImageReference,
1960 state: &LayeredImageState,
1961 colliding_inodes: &BTreeSet<u64>,
1962 verbose: bool,
1963) -> Result<bool> {
1964 let cancellable = gio::Cancellable::NONE;
1965 let repo = &sysroot.repo();
1966 let merge_commit = state.merge_commit.as_str();
1967 let merge_commit_root = repo.read_commit(merge_commit, gio::Cancellable::NONE)?.0;
1968 let merge_commit_root = merge_commit_root
1969 .downcast::<ostree::RepoFile>()
1970 .expect("downcast");
1971 merge_commit_root.ensure_resolved()?;
1972
1973 let (commit_layer, _component_layers, remaining_layers) =
1974 parse_manifest_layout(&state.manifest, &state.configuration)?;
1975
1976 let mut comparison_state = CompareState::default();
1977
1978 let query = |l: &Descriptor| query_layer(repo, l.clone());
1979
1980 let base_tree = repo
1981 .read_commit(&state.base_commit, cancellable)?
1982 .0
1983 .downcast::<ostree::RepoFile>()
1984 .expect("downcast");
1985 if let Some(commit_layer) = commit_layer {
1986 println!(
1987 "Verifying with base ostree layer {}",
1988 ref_for_layer(commit_layer)?
1989 );
1990 }
1991 compare_commit_trees(
1992 repo,
1993 "/".into(),
1994 &merge_commit_root,
1995 &base_tree,
1996 true,
1997 colliding_inodes,
1998 &mut comparison_state,
1999 )?;
2000
2001 let remaining_layers = remaining_layers
2002 .into_iter()
2003 .map(query)
2004 .collect::<Result<Vec<_>>>()?;
2005
2006 println!("Image has {} derived layers", remaining_layers.len());
2007
2008 for layer in remaining_layers.iter().rev() {
2009 let layer_ref = layer.ostree_ref.as_str();
2010 let layer_commit = layer
2011 .commit
2012 .as_deref()
2013 .ok_or_else(|| anyhow!("Missing layer {layer_ref}"))?;
2014 let layer_tree = repo
2015 .read_commit(layer_commit, cancellable)?
2016 .0
2017 .downcast::<ostree::RepoFile>()
2018 .expect("downcast");
2019 compare_commit_trees(
2020 repo,
2021 "/".into(),
2022 &merge_commit_root,
2023 &layer_tree,
2024 false,
2025 colliding_inodes,
2026 &mut comparison_state,
2027 )?;
2028 }
2029
2030 let n_verified = comparison_state.verified.len();
2031 if comparison_state.is_ok() {
2032 println!("OK image {imgref} (verified={n_verified})");
2033 println!();
2034 } else {
2035 let n_inode = comparison_state.inode_corrupted.len();
2036 let n_other = comparison_state.unknown_corrupted.len();
2037 eprintln!("warning: Found corrupted merge commit");
2038 eprintln!(" inode clashes: {n_inode}");
2039 eprintln!(" unknown: {n_other}");
2040 eprintln!(" ok: {n_verified}");
2041 if verbose {
2042 eprintln!("Mismatches:");
2043 for path in comparison_state.inode_corrupted {
2044 eprintln!(" inode: {path}");
2045 }
2046 for path in comparison_state.unknown_corrupted {
2047 eprintln!(" other: {path}");
2048 }
2049 }
2050 eprintln!();
2051 return Ok(false);
2052 }
2053
2054 Ok(true)
2055}
2056
2057#[cfg(test)]
2058mod tests {
2059 use cap_std_ext::cap_tempfile;
2060 use oci_image::{DescriptorBuilder, MediaType, Sha256Digest};
2061
2062 use super::*;
2063
2064 #[test]
2065 fn test_ref_for_descriptor() {
2066 let d = DescriptorBuilder::default()
2067 .size(42u64)
2068 .media_type(MediaType::ImageManifest)
2069 .digest(
2070 Sha256Digest::from_str(
2071 "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
2072 )
2073 .unwrap(),
2074 )
2075 .build()
2076 .unwrap();
2077 assert_eq!(
2078 ref_for_layer(&d).unwrap(),
2079 "ostree/container/blob/sha256_3A_2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
2080 );
2081 }
2082
2083 #[test]
2084 fn test_cleanup_root() -> Result<()> {
2085 let td = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
2086 let usretc = "usr/etc";
2087 cleanup_root(&td).unwrap();
2088 td.create_dir_all(usretc)?;
2089 let usretc = &td.open_dir(usretc)?;
2090 usretc.write("hostname", b"hostname")?;
2091 cleanup_root(&td).unwrap();
2092 assert!(usretc.try_exists("hostname")?);
2093 usretc.write("hostname", b"")?;
2094 cleanup_root(&td).unwrap();
2095 assert!(!td.try_exists("hostname")?);
2096
2097 usretc.symlink_contents("../run/systemd/stub-resolv.conf", "resolv.conf")?;
2098 cleanup_root(&td).unwrap();
2099 assert!(usretc.symlink_metadata("resolv.conf")?.is_symlink());
2100 usretc.remove_file("resolv.conf")?;
2101 usretc.write("resolv.conf", b"")?;
2102 cleanup_root(&td).unwrap();
2103 assert!(!usretc.try_exists("resolv.conf")?);
2104
2105 Ok(())
2106 }
2107}