1use super::*;
9use crate::chunking::{self, Chunk};
10use crate::logging::system_repo_journal_print;
11use crate::refescape;
12use crate::sysroot::SysrootLock;
13use crate::utils::ResultExt;
14use anyhow::{anyhow, Context};
15use camino::{Utf8Path, Utf8PathBuf};
16use cap_std_ext::cap_std;
17use cap_std_ext::cap_std::fs::{Dir, MetadataExt};
18use cap_std_ext::cmdext::CapStdExtCommandExt;
19use containers_image_proxy::{ImageProxy, OpenedImage};
20use flate2::Compression;
21use fn_error_context::context;
22use futures_util::TryFutureExt;
23use oci_spec::image::{
24 self as oci_image, Arch, Descriptor, Digest, History, ImageConfiguration, ImageManifest,
25};
26use ostree::prelude::{Cast, FileEnumeratorExt, FileExt, ToVariant};
27use ostree::{gio, glib};
28use std::collections::{BTreeSet, HashMap};
29use std::iter::FromIterator;
30use tokio::sync::mpsc::{Receiver, Sender};
31
32pub use containers_image_proxy::ImageProxyConfig;
37
38const LAYER_PREFIX: &str = "ostree/container/blob";
40const IMAGE_PREFIX: &str = "ostree/container/image";
42pub const BASE_IMAGE_PREFIX: &str = "ostree/container/baseimage";
47
48pub(crate) const META_MANIFEST_DIGEST: &str = "ostree.manifest-digest";
50const META_MANIFEST: &str = "ostree.manifest";
52const META_CONFIG: &str = "ostree.container.image-config";
54pub const META_FILTERED: &str = "ostree.tar-filtered";
56pub type MetaFilteredData = HashMap<String, HashMap<String, u32>>;
58
59const OSTREE_BASE_DEPLOYMENT_REFS: &[&str] = &["ostree/0", "ostree/1"];
61const RPMOSTREE_BASE_REFS: &[&str] = &["rpmostree/base"];
63
64fn ref_for_blob_digest(d: &str) -> Result<String> {
66 refescape::prefix_escape_for_ref(LAYER_PREFIX, d)
67}
68
69fn ref_for_layer(l: &oci_image::Descriptor) -> Result<String> {
71 ref_for_blob_digest(&l.digest().to_string())
72}
73
74fn ref_for_image(l: &ImageReference) -> Result<String> {
76 refescape::prefix_escape_for_ref(IMAGE_PREFIX, &l.to_string())
77}
78
79#[derive(Debug)]
81pub enum ImportProgress {
82 OstreeChunkStarted(Descriptor),
84 OstreeChunkCompleted(Descriptor),
86 DerivedLayerStarted(Descriptor),
88 DerivedLayerCompleted(Descriptor),
90}
91
92impl ImportProgress {
93 pub fn is_starting(&self) -> bool {
95 match self {
96 ImportProgress::OstreeChunkStarted(_) => true,
97 ImportProgress::OstreeChunkCompleted(_) => false,
98 ImportProgress::DerivedLayerStarted(_) => true,
99 ImportProgress::DerivedLayerCompleted(_) => false,
100 }
101 }
102}
103
104#[derive(Debug)]
106pub struct LayerProgress {
107 pub layer_index: usize,
109 pub fetched: u64,
111 pub total: u64,
113}
114
115#[derive(Debug, PartialEq, Eq)]
117pub struct LayeredImageState {
118 pub base_commit: String,
120 pub merge_commit: String,
122 pub manifest_digest: Digest,
124 pub manifest: ImageManifest,
126 pub configuration: ImageConfiguration,
128 pub cached_update: Option<CachedImageUpdate>,
130}
131
132impl LayeredImageState {
133 pub fn get_commit(&self) -> &str {
137 self.merge_commit.as_str()
138 }
139
140 pub fn version(&self) -> Option<&str> {
142 super::version_for_config(&self.configuration)
143 }
144}
145
146#[derive(Debug, PartialEq, Eq)]
148pub struct CachedImageUpdate {
149 pub manifest: ImageManifest,
151 pub config: ImageConfiguration,
153 pub manifest_digest: Digest,
155}
156
157impl CachedImageUpdate {
158 pub fn version(&self) -> Option<&str> {
160 super::version_for_config(&self.config)
161 }
162}
163
164#[derive(Debug)]
166pub struct ImageImporter {
167 repo: ostree::Repo,
168 pub(crate) proxy: ImageProxy,
169 imgref: OstreeImageReference,
170 target_imgref: Option<OstreeImageReference>,
171 no_imgref: bool, disable_gc: bool, require_bootable: bool,
175 ostree_v2024_3: bool,
177 pub(crate) proxy_img: OpenedImage,
178
179 layer_progress: Option<Sender<ImportProgress>>,
180 layer_byte_progress: Option<tokio::sync::watch::Sender<Option<LayerProgress>>>,
181}
182
183#[derive(Debug)]
185pub enum PrepareResult {
186 AlreadyPresent(Box<LayeredImageState>),
188 Ready(Box<PreparedImport>),
190}
191
192#[derive(Debug)]
194pub struct ManifestLayerState {
195 pub(crate) layer: oci_image::Descriptor,
197 pub ostree_ref: String,
200 pub commit: Option<String>,
203}
204
205impl ManifestLayerState {
206 pub fn layer(&self) -> &oci_image::Descriptor {
208 &self.layer
209 }
210}
211
212#[derive(Debug)]
214pub struct PreparedImport {
215 pub manifest_digest: Digest,
217 pub manifest: oci_image::ImageManifest,
219 pub config: oci_image::ImageConfiguration,
221 pub previous_state: Option<Box<LayeredImageState>>,
223 pub previous_manifest_digest: Option<Digest>,
225 pub previous_imageid: Option<String>,
227 pub ostree_layers: Vec<ManifestLayerState>,
229 pub ostree_commit_layer: ManifestLayerState,
231 pub layers: Vec<ManifestLayerState>,
233}
234
235impl PreparedImport {
236 pub fn all_layers(&self) -> impl Iterator<Item = &ManifestLayerState> {
238 std::iter::once(&self.ostree_commit_layer)
239 .chain(self.ostree_layers.iter())
240 .chain(self.layers.iter())
241 }
242
243 pub fn version(&self) -> Option<&str> {
245 super::version_for_config(&self.config)
246 }
247
248 pub fn deprecated_warning(&self) -> Option<&'static str> {
250 None
251 }
252
253 pub fn layers_with_history(
256 &self,
257 ) -> impl Iterator<Item = Result<(&ManifestLayerState, &History)>> {
258 let truncated = std::iter::once_with(|| Err(anyhow::anyhow!("Truncated history")));
260 let history = self.config.history().iter().map(Ok).chain(truncated);
261 self.all_layers()
262 .zip(history)
263 .map(|(s, h)| h.map(|h| (s, h)))
264 }
265
266 pub fn layers_to_fetch(&self) -> impl Iterator<Item = Result<(&ManifestLayerState, &str)>> {
268 self.layers_with_history().filter_map(|r| {
269 r.map(|(l, h)| {
270 l.commit.is_none().then(|| {
271 let comment = h.created_by().as_deref().unwrap_or("");
272 (l, comment)
273 })
274 })
275 .transpose()
276 })
277 }
278
279 pub(crate) fn format_layer_status(&self) -> Option<String> {
281 let (stored, to_fetch, to_fetch_size) =
282 self.all_layers()
283 .fold((0u32, 0u32, 0u64), |(stored, to_fetch, sz), v| {
284 if v.commit.is_some() {
285 (stored + 1, to_fetch, sz)
286 } else {
287 (stored, to_fetch + 1, sz + v.layer().size())
288 }
289 });
290 (to_fetch > 0).then(|| {
291 let size = crate::glib::format_size(to_fetch_size);
292 format!("layers already present: {stored}; layers needed: {to_fetch} ({size})")
293 })
294 }
295}
296
297pub(crate) fn query_layer(
299 repo: &ostree::Repo,
300 layer: oci_image::Descriptor,
301) -> Result<ManifestLayerState> {
302 let ostree_ref = ref_for_layer(&layer)?;
303 let commit = repo.resolve_rev(&ostree_ref, true)?.map(|s| s.to_string());
304 Ok(ManifestLayerState {
305 layer,
306 ostree_ref,
307 commit,
308 })
309}
310
311#[context("Reading manifest data from commit")]
312fn manifest_data_from_commitmeta(
313 commit_meta: &glib::VariantDict,
314) -> Result<(oci_image::ImageManifest, Digest)> {
315 let digest = commit_meta
316 .lookup::<String>(META_MANIFEST_DIGEST)?
317 .ok_or_else(|| anyhow!("Missing {} metadata on merge commit", META_MANIFEST_DIGEST))?;
318 let digest = Digest::from_str(&digest)?;
319 let manifest_bytes: String = commit_meta
320 .lookup::<String>(META_MANIFEST)?
321 .ok_or_else(|| anyhow!("Failed to find {} metadata key", META_MANIFEST))?;
322 let r = serde_json::from_str(&manifest_bytes)?;
323 Ok((r, digest))
324}
325
326fn image_config_from_commitmeta(commit_meta: &glib::VariantDict) -> Result<ImageConfiguration> {
327 let config = if let Some(config) = commit_meta
328 .lookup::<String>(META_CONFIG)?
329 .filter(|v| v != "null") .map(|v| serde_json::from_str(&v).map_err(anyhow::Error::msg))
331 .transpose()?
332 {
333 config
334 } else {
335 tracing::debug!("No image configuration found");
336 Default::default()
337 };
338 Ok(config)
339}
340
341pub fn manifest_digest_from_commit(commit: &glib::Variant) -> Result<Digest> {
347 let commit_meta = &commit.child_value(0);
348 let commit_meta = &glib::VariantDict::new(Some(commit_meta));
349 Ok(manifest_data_from_commitmeta(commit_meta)?.1)
350}
351
352fn layer_from_diffid<'a>(
356 manifest: &'a ImageManifest,
357 config: &ImageConfiguration,
358 diffid: &str,
359) -> Result<&'a Descriptor> {
360 let idx = config
361 .rootfs()
362 .diff_ids()
363 .iter()
364 .position(|x| x.as_str() == diffid)
365 .ok_or_else(|| anyhow!("Missing {} {}", DIFFID_LABEL, diffid))?;
366 manifest.layers().get(idx).ok_or_else(|| {
367 anyhow!(
368 "diffid position {} exceeds layer count {}",
369 idx,
370 manifest.layers().len()
371 )
372 })
373}
374
375#[context("Parsing manifest layout")]
376pub(crate) fn parse_manifest_layout<'a>(
377 manifest: &'a ImageManifest,
378 config: &ImageConfiguration,
379) -> Result<(&'a Descriptor, Vec<&'a Descriptor>, Vec<&'a Descriptor>)> {
380 let config_labels = super::labels_of(config);
381
382 let first_layer = manifest
383 .layers()
384 .first()
385 .ok_or_else(|| anyhow!("No layers in manifest"))?;
386 let target_diffid = config_labels
387 .and_then(|labels| labels.get(DIFFID_LABEL))
388 .ok_or_else(|| {
389 anyhow!(
390 "No {} label found, not an ostree encapsulated container",
391 DIFFID_LABEL
392 )
393 })?;
394
395 let target_layer = layer_from_diffid(manifest, config, target_diffid.as_str())?;
396 let mut chunk_layers = Vec::new();
397 let mut derived_layers = Vec::new();
398 let mut after_target = false;
399 let ostree_layer = first_layer;
401 for layer in manifest.layers() {
402 if layer == target_layer {
403 if after_target {
404 anyhow::bail!("Multiple entries for {}", layer.digest());
405 }
406 after_target = true;
407 if layer != ostree_layer {
408 chunk_layers.push(layer);
409 }
410 } else if !after_target {
411 if layer != ostree_layer {
412 chunk_layers.push(layer);
413 }
414 } else {
415 derived_layers.push(layer);
416 }
417 }
418
419 Ok((ostree_layer, chunk_layers, derived_layers))
420}
421
422fn timestamp_of_manifest_or_config(
424 manifest: &ImageManifest,
425 config: &ImageConfiguration,
426) -> Option<u64> {
427 let timestamp = manifest
430 .annotations()
431 .as_ref()
432 .and_then(|a| a.get(oci_image::ANNOTATION_CREATED))
433 .or_else(|| config.created().as_ref());
434 timestamp
436 .map(|t| {
437 chrono::DateTime::parse_from_rfc3339(t)
438 .context("Failed to parse manifest timestamp")
439 .map(|t| t.timestamp() as u64)
440 })
441 .transpose()
442 .log_err_default()
443}
444
445impl ImageImporter {
446 const CACHED_KEY_MANIFEST_DIGEST: &'static str = "ostree-ext.cached.manifest-digest";
448 const CACHED_KEY_MANIFEST: &'static str = "ostree-ext.cached.manifest";
449 const CACHED_KEY_CONFIG: &'static str = "ostree-ext.cached.config";
450
451 #[context("Creating importer")]
453 pub async fn new(
454 repo: &ostree::Repo,
455 imgref: &OstreeImageReference,
456 mut config: ImageProxyConfig,
457 ) -> Result<Self> {
458 if imgref.imgref.transport == Transport::ContainerStorage {
459 merge_default_container_proxy_opts_with_isolation(&mut config, None)?;
461 } else {
462 merge_default_container_proxy_opts(&mut config)?;
464 }
465 let proxy = ImageProxy::new_with_config(config).await?;
466
467 system_repo_journal_print(
468 repo,
469 libsystemd::logging::Priority::Info,
470 &format!("Fetching {}", imgref),
471 );
472
473 let proxy_img = proxy.open_image(&imgref.imgref.to_string()).await?;
474 let repo = repo.clone();
475 Ok(ImageImporter {
476 repo,
477 proxy,
478 proxy_img,
479 target_imgref: None,
480 no_imgref: false,
481 ostree_v2024_3: ostree::check_version(2024, 3),
482 disable_gc: false,
483 require_bootable: false,
484 imgref: imgref.clone(),
485 layer_progress: None,
486 layer_byte_progress: None,
487 })
488 }
489
490 pub fn set_target(&mut self, target: &OstreeImageReference) {
492 self.target_imgref = Some(target.clone())
493 }
494
495 pub fn set_no_imgref(&mut self) {
499 self.no_imgref = true;
500 }
501
502 pub fn require_bootable(&mut self) {
504 self.require_bootable = true;
505 }
506
507 pub fn set_ostree_version(&mut self, year: u32, v: u32) {
509 self.ostree_v2024_3 = (year > 2024) || (year == 2024 && v >= 3)
510 }
511
512 pub fn disable_gc(&mut self) {
514 self.disable_gc = true;
515 }
516
517 #[context("Preparing import")]
522 pub async fn prepare(&mut self) -> Result<PrepareResult> {
523 self.prepare_internal(false).await
524 }
525
526 pub fn request_progress(&mut self) -> Receiver<ImportProgress> {
528 assert!(self.layer_progress.is_none());
529 let (s, r) = tokio::sync::mpsc::channel(2);
530 self.layer_progress = Some(s);
531 r
532 }
533
534 pub fn request_layer_progress(
536 &mut self,
537 ) -> tokio::sync::watch::Receiver<Option<LayerProgress>> {
538 assert!(self.layer_byte_progress.is_none());
539 let (s, r) = tokio::sync::watch::channel(None);
540 self.layer_byte_progress = Some(s);
541 r
542 }
543
544 #[context("Writing cached pending manifest")]
547 pub(crate) async fn cache_pending(
548 &self,
549 commit: &str,
550 manifest_digest: &Digest,
551 manifest: &ImageManifest,
552 config: &ImageConfiguration,
553 ) -> Result<()> {
554 let commitmeta = glib::VariantDict::new(None);
555 commitmeta.insert(
556 Self::CACHED_KEY_MANIFEST_DIGEST,
557 manifest_digest.to_string(),
558 );
559 let cached_manifest = serde_json::to_string(manifest).context("Serializing manifest")?;
560 commitmeta.insert(Self::CACHED_KEY_MANIFEST, cached_manifest);
561 let cached_config = serde_json::to_string(config).context("Serializing config")?;
562 commitmeta.insert(Self::CACHED_KEY_CONFIG, cached_config);
563 let commitmeta = commitmeta.to_variant();
564 let commit = commit.to_string();
566 let repo = self.repo.clone();
567 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
568 repo.write_commit_detached_metadata(&commit, Some(&commitmeta), Some(cancellable))
569 .map_err(anyhow::Error::msg)
570 })
571 .await
572 }
573
574 fn create_prepared_import(
577 &mut self,
578 manifest_digest: Digest,
579 manifest: ImageManifest,
580 config: ImageConfiguration,
581 previous_state: Option<Box<LayeredImageState>>,
582 previous_imageid: Option<String>,
583 ) -> Result<Box<PreparedImport>> {
584 let config_labels = super::labels_of(&config);
585 if self.require_bootable {
586 let bootable_key = *ostree::METADATA_KEY_BOOTABLE;
587 let bootable = config_labels.map_or(false, |l| {
588 l.contains_key(bootable_key) || l.contains_key(BOOTC_LABEL)
589 });
590 if !bootable {
591 anyhow::bail!("Target image does not have {bootable_key} label");
592 }
593 let container_arch = config.architecture();
594 let target_arch = &Arch::default();
595 if container_arch != target_arch {
596 anyhow::bail!("Image has architecture {container_arch}; expected {target_arch}");
597 }
598 }
599
600 let (commit_layer, component_layers, remaining_layers) =
601 parse_manifest_layout(&manifest, &config)?;
602
603 let query = |l: &Descriptor| query_layer(&self.repo, l.clone());
604 let commit_layer = query(commit_layer)?;
605 let component_layers = component_layers
606 .into_iter()
607 .map(query)
608 .collect::<Result<Vec<_>>>()?;
609 let remaining_layers = remaining_layers
610 .into_iter()
611 .map(query)
612 .collect::<Result<Vec<_>>>()?;
613
614 let previous_manifest_digest = previous_state.as_ref().map(|s| s.manifest_digest.clone());
615 let imp = PreparedImport {
616 manifest_digest,
617 manifest,
618 config,
619 previous_state,
620 previous_manifest_digest,
621 previous_imageid,
622 ostree_layers: component_layers,
623 ostree_commit_layer: commit_layer,
624 layers: remaining_layers,
625 };
626 Ok(Box::new(imp))
627 }
628
629 #[context("Fetching manifest")]
631 pub(crate) async fn prepare_internal(&mut self, verify_layers: bool) -> Result<PrepareResult> {
632 match &self.imgref.sigverify {
633 SignatureSource::ContainerPolicy if skopeo::container_policy_is_default_insecure()? => {
634 return Err(anyhow!("containers-policy.json specifies a default of `insecureAcceptAnything`; refusing usage"));
635 }
636 SignatureSource::OstreeRemote(_) if verify_layers => {
637 return Err(anyhow!(
638 "Cannot currently verify layered containers via ostree remote"
639 ));
640 }
641 _ => {}
642 }
643
644 let (manifest_digest, manifest) = self.proxy.fetch_manifest(&self.proxy_img).await?;
645 let manifest_digest = Digest::from_str(&manifest_digest)?;
646 let new_imageid = manifest.config().digest();
647
648 let (previous_state, previous_imageid) =
651 if let Some(previous_state) = try_query_image(&self.repo, &self.imgref.imgref)? {
652 if previous_state.manifest_digest == manifest_digest {
654 return Ok(PrepareResult::AlreadyPresent(previous_state));
655 }
656 let previous_imageid = previous_state.manifest.config().digest();
658 if previous_imageid == new_imageid {
659 return Ok(PrepareResult::AlreadyPresent(previous_state));
660 }
661 let previous_imageid = previous_imageid.to_string();
662 (Some(previous_state), Some(previous_imageid))
663 } else {
664 (None, None)
665 };
666
667 let config = self.proxy.fetch_config(&self.proxy_img).await?;
668
669 if let Some(previous_state) = previous_state.as_ref() {
672 self.cache_pending(
673 previous_state.merge_commit.as_str(),
674 &manifest_digest,
675 &manifest,
676 &config,
677 )
678 .await?;
679 }
680
681 let imp = self.create_prepared_import(
682 manifest_digest,
683 manifest,
684 config,
685 previous_state,
686 previous_imageid,
687 )?;
688 Ok(PrepareResult::Ready(imp))
689 }
690
691 #[context("Unencapsulating base")]
693 pub(crate) async fn unencapsulate_base(
694 &mut self,
695 import: &mut store::PreparedImport,
696 write_refs: bool,
697 ) -> Result<()> {
698 tracing::debug!("Fetching base");
699 if matches!(self.imgref.sigverify, SignatureSource::ContainerPolicy)
700 && skopeo::container_policy_is_default_insecure()?
701 {
702 return Err(anyhow!("containers-policy.json specifies a default of `insecureAcceptAnything`; refusing usage"));
703 }
704 let remote = match &self.imgref.sigverify {
705 SignatureSource::OstreeRemote(remote) => Some(remote.clone()),
706 SignatureSource::ContainerPolicy | SignatureSource::ContainerPolicyAllowInsecure => {
707 None
708 }
709 };
710 let des_layers = self.proxy.get_layer_info(&self.proxy_img).await?;
711 for layer in import.ostree_layers.iter_mut() {
712 if layer.commit.is_some() {
713 continue;
714 }
715 if let Some(p) = self.layer_progress.as_ref() {
716 p.send(ImportProgress::OstreeChunkStarted(layer.layer.clone()))
717 .await?;
718 }
719 let (blob, driver, media_type) = fetch_layer(
720 &self.proxy,
721 &self.proxy_img,
722 &import.manifest,
723 &layer.layer,
724 self.layer_byte_progress.as_ref(),
725 des_layers.as_ref(),
726 self.imgref.imgref.transport,
727 )
728 .await?;
729 let repo = self.repo.clone();
730 let target_ref = layer.ostree_ref.clone();
731 let import_task =
732 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
733 let txn = repo.auto_transaction(Some(cancellable))?;
734 let mut importer = crate::tar::Importer::new_for_object_set(&repo);
735 let blob = tokio_util::io::SyncIoBridge::new(blob);
736 let blob = super::unencapsulate::decompressor(&media_type, blob)?;
737 let mut archive = tar::Archive::new(blob);
738 importer.import_objects(&mut archive, Some(cancellable))?;
739 let commit = if write_refs {
740 let commit = importer.finish_import_object_set()?;
741 repo.transaction_set_ref(None, &target_ref, Some(commit.as_str()));
742 tracing::debug!("Wrote {} => {}", target_ref, commit);
743 Some(commit)
744 } else {
745 None
746 };
747 txn.commit(Some(cancellable))?;
748 Ok::<_, anyhow::Error>(commit)
749 })
750 .map_err(|e| e.context(format!("Layer {}", layer.layer.digest())));
751 let commit = super::unencapsulate::join_fetch(import_task, driver).await?;
752 layer.commit = commit;
753 if let Some(p) = self.layer_progress.as_ref() {
754 p.send(ImportProgress::OstreeChunkCompleted(layer.layer.clone()))
755 .await?;
756 }
757 }
758 if import.ostree_commit_layer.commit.is_none() {
759 if let Some(p) = self.layer_progress.as_ref() {
760 p.send(ImportProgress::OstreeChunkStarted(
761 import.ostree_commit_layer.layer.clone(),
762 ))
763 .await?;
764 }
765 let (blob, driver, media_type) = fetch_layer(
766 &self.proxy,
767 &self.proxy_img,
768 &import.manifest,
769 &import.ostree_commit_layer.layer,
770 self.layer_byte_progress.as_ref(),
771 des_layers.as_ref(),
772 self.imgref.imgref.transport,
773 )
774 .await?;
775 let repo = self.repo.clone();
776 let target_ref = import.ostree_commit_layer.ostree_ref.clone();
777 let import_task =
778 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
779 let txn = repo.auto_transaction(Some(cancellable))?;
780 let mut importer = crate::tar::Importer::new_for_commit(&repo, remote);
781 let blob = tokio_util::io::SyncIoBridge::new(blob);
782 let blob = super::unencapsulate::decompressor(&media_type, blob)?;
783 let mut archive = tar::Archive::new(blob);
784 importer.import_commit(&mut archive, Some(cancellable))?;
785 let commit = importer.finish_import_commit();
786 if write_refs {
787 repo.transaction_set_ref(None, &target_ref, Some(commit.as_str()));
788 tracing::debug!("Wrote {} => {}", target_ref, commit);
789 }
790 repo.mark_commit_partial(&commit, false)?;
791 txn.commit(Some(cancellable))?;
792 Ok::<_, anyhow::Error>(commit)
793 });
794 let commit = super::unencapsulate::join_fetch(import_task, driver).await?;
795 import.ostree_commit_layer.commit = Some(commit);
796 if let Some(p) = self.layer_progress.as_ref() {
797 p.send(ImportProgress::OstreeChunkCompleted(
798 import.ostree_commit_layer.layer.clone(),
799 ))
800 .await?;
801 }
802 };
803 Ok(())
804 }
805
806 pub async fn unencapsulate(mut self) -> Result<Import> {
811 let mut prep = match self.prepare_internal(false).await? {
812 PrepareResult::AlreadyPresent(_) => {
813 panic!("Should not have image present for unencapsulation")
814 }
815 PrepareResult::Ready(r) => r,
816 };
817 if !prep.layers.is_empty() {
818 anyhow::bail!("Image has {} non-ostree layers", prep.layers.len());
819 }
820 let deprecated_warning = prep.deprecated_warning().map(ToOwned::to_owned);
821 self.unencapsulate_base(&mut prep, false).await?;
822 self.proxy.close_image(&self.proxy_img).await?;
825 let ostree_commit = prep.ostree_commit_layer.commit.unwrap();
826 let image_digest = prep.manifest_digest;
827 Ok(Import {
828 ostree_commit,
829 image_digest,
830 deprecated_warning,
831 })
832 }
833
834 #[context("Importing")]
838 pub async fn import(
839 mut self,
840 mut import: Box<PreparedImport>,
841 ) -> Result<Box<LayeredImageState>> {
842 if let Some(status) = import.format_layer_status() {
843 system_repo_journal_print(&self.repo, libsystemd::logging::Priority::Info, &status);
844 }
845 self.unencapsulate_base(&mut import, true).await?;
848 let des_layers = self.proxy.get_layer_info(&self.proxy_img).await?;
849 let proxy = self.proxy;
850 let proxy_img = self.proxy_img;
851 let target_imgref = self.target_imgref.as_ref().unwrap_or(&self.imgref);
852 let base_commit = import.ostree_commit_layer.commit.clone().unwrap();
853
854 let root_is_transient = {
855 let rootf = self
856 .repo
857 .read_commit(&base_commit, gio::Cancellable::NONE)?
858 .0;
859 let rootf = rootf.downcast_ref::<ostree::RepoFile>().unwrap();
860 crate::ostree_prepareroot::overlayfs_root_enabled(rootf)?
861 };
862 tracing::debug!("Base rootfs is transient: {root_is_transient}");
863
864 let ostree_ref = ref_for_image(&target_imgref.imgref)?;
865
866 let mut layer_commits = Vec::new();
867 let mut layer_filtered_content: MetaFilteredData = HashMap::new();
868 let have_derived_layers = !import.layers.is_empty();
869 for layer in import.layers {
870 if let Some(c) = layer.commit {
871 tracing::debug!("Reusing fetched commit {}", c);
872 layer_commits.push(c.to_string());
873 } else {
874 if let Some(p) = self.layer_progress.as_ref() {
875 p.send(ImportProgress::DerivedLayerStarted(layer.layer.clone()))
876 .await?;
877 }
878 let (blob, driver, media_type) = super::unencapsulate::fetch_layer(
879 &proxy,
880 &proxy_img,
881 &import.manifest,
882 &layer.layer,
883 self.layer_byte_progress.as_ref(),
884 des_layers.as_ref(),
885 self.imgref.imgref.transport,
886 )
887 .await?;
888 let opts = crate::tar::WriteTarOptions {
891 base: Some(base_commit.clone()),
892 selinux: true,
893 allow_nonusr: root_is_transient,
894 retain_var: self.ostree_v2024_3,
895 };
896 let r = crate::tar::write_tar(
897 &self.repo,
898 blob,
899 media_type,
900 layer.ostree_ref.as_str(),
901 Some(opts),
902 );
903 let r = super::unencapsulate::join_fetch(r, driver)
904 .await
905 .with_context(|| format!("Parsing layer blob {}", layer.layer.digest()))?;
906 layer_commits.push(r.commit);
907 if !r.filtered.is_empty() {
908 let filtered = HashMap::from_iter(r.filtered.into_iter());
909 tracing::debug!("Found {} filtered toplevels", filtered.len());
910 layer_filtered_content.insert(layer.layer.digest().to_string(), filtered);
911 } else {
912 tracing::debug!("No filtered content");
913 }
914 if let Some(p) = self.layer_progress.as_ref() {
915 p.send(ImportProgress::DerivedLayerCompleted(layer.layer.clone()))
916 .await?;
917 }
918 }
919 }
920
921 proxy.close_image(&proxy_img).await?;
924
925 proxy.finalize().await?;
927 tracing::debug!("finalized proxy");
928
929 let serialized_manifest = serde_json::to_string(&import.manifest)?;
930 let serialized_config = serde_json::to_string(&import.config)?;
931 let mut metadata = HashMap::new();
932 metadata.insert(
933 META_MANIFEST_DIGEST,
934 import.manifest_digest.to_string().to_variant(),
935 );
936 metadata.insert(META_MANIFEST, serialized_manifest.to_variant());
937 metadata.insert(META_CONFIG, serialized_config.to_variant());
938 metadata.insert(
939 "ostree.importer.version",
940 env!("CARGO_PKG_VERSION").to_variant(),
941 );
942 let filtered = layer_filtered_content.to_variant();
943 metadata.insert(META_FILTERED, filtered);
944 let metadata = metadata.to_variant();
945
946 let timestamp = timestamp_of_manifest_or_config(&import.manifest, &import.config)
947 .unwrap_or_else(|| chrono::offset::Utc::now().timestamp() as u64);
948 let repo = self.repo;
950 let state = crate::tokio_util::spawn_blocking_cancellable_flatten(
951 move |cancellable| -> Result<Box<LayeredImageState>> {
952 use rustix::fd::AsRawFd;
953
954 let cancellable = Some(cancellable);
955 let repo = &repo;
956 let txn = repo.auto_transaction(cancellable)?;
957
958 let devino = ostree::RepoDevInoCache::new();
959 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
960 let repo_tmp = repodir.open_dir("tmp")?;
961 let td = cap_std_ext::cap_tempfile::TempDir::new_in(&repo_tmp)?;
962
963 let rootpath = "root";
964 let checkout_mode = if repo.mode() == ostree::RepoMode::Bare {
965 ostree::RepoCheckoutMode::None
966 } else {
967 ostree::RepoCheckoutMode::User
968 };
969 let mut checkout_opts = ostree::RepoCheckoutAtOptions {
970 mode: checkout_mode,
971 overwrite_mode: ostree::RepoCheckoutOverwriteMode::UnionFiles,
972 devino_to_csum_cache: Some(devino.clone()),
973 no_copy_fallback: true,
974 force_copy_zerosized: true,
975 process_whiteouts: false,
976 ..Default::default()
977 };
978 repo.checkout_at(
979 Some(&checkout_opts),
980 (*td).as_raw_fd(),
981 rootpath,
982 &base_commit,
983 cancellable,
984 )
985 .context("Checking out base commit")?;
986
987 checkout_opts.process_whiteouts = true;
989 for commit in layer_commits {
990 repo.checkout_at(
991 Some(&checkout_opts),
992 (*td).as_raw_fd(),
993 rootpath,
994 &commit,
995 cancellable,
996 )
997 .with_context(|| format!("Checking out layer {commit}"))?;
998 }
999
1000 let modifier =
1001 ostree::RepoCommitModifier::new(ostree::RepoCommitModifierFlags::CONSUME, None);
1002 modifier.set_devino_cache(&devino);
1003 if have_derived_layers {
1007 let rootpath = td.open_dir(rootpath)?;
1008 let sepolicy = ostree::SePolicy::new_at(rootpath.as_raw_fd(), cancellable)?;
1009 tracing::debug!("labeling from merged tree");
1010 modifier.set_sepolicy(Some(&sepolicy));
1011 } else {
1012 tracing::debug!("labeling from base tree");
1013 modifier.set_sepolicy_from_commit(repo, &base_commit, cancellable)?;
1015 }
1016
1017 let mt = ostree::MutableTree::new();
1018 repo.write_dfd_to_mtree(
1019 (*td).as_raw_fd(),
1020 rootpath,
1021 &mt,
1022 Some(&modifier),
1023 cancellable,
1024 )
1025 .context("Writing merged filesystem to mtree")?;
1026
1027 let merged_root = repo
1028 .write_mtree(&mt, cancellable)
1029 .context("Writing mtree")?;
1030 let merged_root = merged_root.downcast::<ostree::RepoFile>().unwrap();
1031 let merged_commit = repo
1032 .write_commit_with_time(
1033 None,
1034 None,
1035 None,
1036 Some(&metadata),
1037 &merged_root,
1038 timestamp,
1039 cancellable,
1040 )
1041 .context("Writing commit")?;
1042 if !self.no_imgref {
1043 repo.transaction_set_ref(None, &ostree_ref, Some(merged_commit.as_str()));
1044 }
1045 txn.commit(cancellable)?;
1046
1047 if !self.disable_gc {
1048 let n: u32 = gc_image_layers_impl(repo, cancellable)?;
1049 tracing::debug!("pruned {n} layers");
1050 }
1051
1052 let state = query_image_commit(repo, &merged_commit)?;
1055 Ok(state)
1056 },
1057 )
1058 .await?;
1059 Ok(state)
1060 }
1061}
1062
1063pub fn list_images(repo: &ostree::Repo) -> Result<Vec<String>> {
1065 let cancellable = gio::Cancellable::NONE;
1066 let refs = repo.list_refs_ext(
1067 Some(IMAGE_PREFIX),
1068 ostree::RepoListRefsExtFlags::empty(),
1069 cancellable,
1070 )?;
1071 refs.keys()
1072 .map(|imgname| refescape::unprefix_unescape_ref(IMAGE_PREFIX, imgname))
1073 .collect()
1074}
1075
1076fn try_query_image(
1079 repo: &ostree::Repo,
1080 imgref: &ImageReference,
1081) -> Result<Option<Box<LayeredImageState>>> {
1082 let ostree_ref = &ref_for_image(imgref)?;
1083 if let Some(merge_rev) = repo.resolve_rev(ostree_ref, true)? {
1084 match query_image_commit(repo, merge_rev.as_str()) {
1085 Ok(r) => Ok(Some(r)),
1086 Err(e) => {
1087 eprintln!("error: failed to query image commit: {e}");
1088 Ok(None)
1089 }
1090 }
1091 } else {
1092 Ok(None)
1093 }
1094}
1095
1096#[context("Querying image {imgref}")]
1098pub fn query_image(
1099 repo: &ostree::Repo,
1100 imgref: &ImageReference,
1101) -> Result<Option<Box<LayeredImageState>>> {
1102 let ostree_ref = &ref_for_image(imgref)?;
1103 let merge_rev = repo.resolve_rev(ostree_ref, true)?;
1104 merge_rev
1105 .map(|r| query_image_commit(repo, r.as_str()))
1106 .transpose()
1107}
1108
1109fn parse_cached_update(meta: &glib::VariantDict) -> Result<Option<CachedImageUpdate>> {
1111 let manifest_digest =
1113 if let Some(d) = meta.lookup::<String>(ImageImporter::CACHED_KEY_MANIFEST_DIGEST)? {
1114 d
1115 } else {
1116 return Ok(None);
1119 };
1120 let manifest_digest = Digest::from_str(&manifest_digest)?;
1121 let manifest = meta.lookup_value(ImageImporter::CACHED_KEY_MANIFEST, None);
1124 let manifest: oci_image::ImageManifest = manifest
1125 .as_ref()
1126 .and_then(|v| v.str())
1127 .map(serde_json::from_str)
1128 .transpose()?
1129 .ok_or_else(|| {
1130 anyhow!(
1131 "Expected cached manifest {}",
1132 ImageImporter::CACHED_KEY_MANIFEST
1133 )
1134 })?;
1135 let config = meta.lookup_value(ImageImporter::CACHED_KEY_CONFIG, None);
1136 let config: oci_image::ImageConfiguration = config
1137 .as_ref()
1138 .and_then(|v| v.str())
1139 .map(serde_json::from_str)
1140 .transpose()?
1141 .ok_or_else(|| {
1142 anyhow!(
1143 "Expected cached manifest {}",
1144 ImageImporter::CACHED_KEY_CONFIG
1145 )
1146 })?;
1147 Ok(Some(CachedImageUpdate {
1148 manifest,
1149 config,
1150 manifest_digest,
1151 }))
1152}
1153
1154pub fn query_image_commit(repo: &ostree::Repo, commit: &str) -> Result<Box<LayeredImageState>> {
1157 let merge_commit = commit.to_string();
1158 let merge_commit_obj = repo.load_commit(commit)?.0;
1159 let commit_meta = &merge_commit_obj.child_value(0);
1160 let commit_meta = &ostree::glib::VariantDict::new(Some(commit_meta));
1161 let (manifest, manifest_digest) = manifest_data_from_commitmeta(commit_meta)?;
1162 let configuration = image_config_from_commitmeta(commit_meta)?;
1163 let mut layers = manifest.layers().iter().cloned();
1164 let base_layer = layers.next().ok_or_else(|| anyhow!("No layers found"))?;
1166 let base_layer = query_layer(repo, base_layer)?;
1167 let ostree_ref = base_layer.ostree_ref.as_str();
1168 let base_commit = base_layer
1169 .commit
1170 .ok_or_else(|| anyhow!("Missing base image ref {ostree_ref}"))?;
1171
1172 let detached_commitmeta =
1173 repo.read_commit_detached_metadata(&merge_commit, gio::Cancellable::NONE)?;
1174 let detached_commitmeta = detached_commitmeta
1175 .as_ref()
1176 .map(|v| glib::VariantDict::new(Some(v)));
1177 let cached_update = detached_commitmeta
1178 .as_ref()
1179 .map(parse_cached_update)
1180 .transpose()?
1181 .flatten();
1182 let state = Box::new(LayeredImageState {
1183 base_commit,
1184 merge_commit,
1185 manifest_digest,
1186 manifest,
1187 configuration,
1188 cached_update,
1189 });
1190 tracing::debug!("Wrote merge commit {}", state.merge_commit);
1191 Ok(state)
1192}
1193
1194fn manifest_for_image(repo: &ostree::Repo, imgref: &ImageReference) -> Result<ImageManifest> {
1195 let ostree_ref = ref_for_image(imgref)?;
1196 let rev = repo.require_rev(&ostree_ref)?;
1197 let (commit_obj, _) = repo.load_commit(rev.as_str())?;
1198 let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
1199 Ok(manifest_data_from_commitmeta(commit_meta)?.0)
1200}
1201
1202#[context("Copying image")]
1205pub async fn copy(
1206 src_repo: &ostree::Repo,
1207 src_imgref: &ImageReference,
1208 dest_repo: &ostree::Repo,
1209 dest_imgref: &ImageReference,
1210) -> Result<()> {
1211 let src_ostree_ref = ref_for_image(src_imgref)?;
1212 let src_commit = src_repo.require_rev(&src_ostree_ref)?;
1213 let manifest = manifest_for_image(src_repo, src_imgref)?;
1214 let layer_refs = manifest
1216 .layers()
1217 .iter()
1218 .map(ref_for_layer)
1219 .chain(std::iter::once(Ok(src_commit.to_string())));
1220 for ostree_ref in layer_refs {
1221 let ostree_ref = ostree_ref?;
1222 let src_repo = src_repo.clone();
1223 let dest_repo = dest_repo.clone();
1224 crate::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| -> Result<_> {
1225 let cancellable = Some(cancellable);
1226 let srcfd = &format!("file:///proc/self/fd/{}", src_repo.dfd());
1227 let flags = ostree::RepoPullFlags::MIRROR;
1228 let opts = glib::VariantDict::new(None);
1229 let refs = [ostree_ref.as_str()];
1230 opts.insert("disable-verify-bindings", true);
1232 opts.insert("refs", &refs[..]);
1233 opts.insert("flags", flags.bits() as i32);
1234 let options = opts.to_variant();
1235 dest_repo.pull_with_options(srcfd, &options, None, cancellable)?;
1236 Ok(())
1237 })
1238 .await?;
1239 }
1240
1241 let dest_ostree_ref = ref_for_image(dest_imgref)?;
1242 dest_repo.set_ref_immediate(
1243 None,
1244 &dest_ostree_ref,
1245 Some(&src_commit),
1246 gio::Cancellable::NONE,
1247 )?;
1248
1249 Ok(())
1250}
1251
1252#[derive(Clone, Debug, Default)]
1254#[non_exhaustive]
1255pub struct ExportToOCIOpts {
1256 pub skip_compression: bool,
1258 pub authfile: Option<std::path::PathBuf>,
1260 pub progress_to_stdout: bool,
1262}
1263
1264fn chunking_from_layer_committed(
1269 repo: &ostree::Repo,
1270 l: &Descriptor,
1271 chunking: &mut chunking::Chunking,
1272) -> Result<()> {
1273 let mut chunk = Chunk::default();
1274 let layer_ref = &ref_for_layer(l)?;
1275 let root = repo.read_commit(layer_ref, gio::Cancellable::NONE)?.0;
1276 let e = root.enumerate_children(
1277 "standard::name,standard::size",
1278 gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS,
1279 gio::Cancellable::NONE,
1280 )?;
1281 for child in e.clone() {
1282 let child = &child?;
1283 let name = child.name();
1285 let name = Utf8Path::from_path(&name).unwrap();
1287 ostree::validate_checksum_string(name.as_str())?;
1288 chunking.remainder.move_obj(&mut chunk, name.as_str());
1289 }
1290 chunking.chunks.push(chunk);
1291 Ok(())
1292}
1293
1294#[context("Copying image")]
1296pub(crate) fn export_to_oci(
1297 repo: &ostree::Repo,
1298 imgref: &ImageReference,
1299 dest_oci: &Dir,
1300 tag: Option<&str>,
1301 opts: ExportToOCIOpts,
1302) -> Result<Descriptor> {
1303 let srcinfo = query_image(repo, imgref)?.ok_or_else(|| anyhow!("No such image"))?;
1304 let (commit_layer, component_layers, remaining_layers) =
1305 parse_manifest_layout(&srcinfo.manifest, &srcinfo.configuration)?;
1306 let commit_chunk_ref = ref_for_layer(commit_layer)?;
1307 let commit_chunk_rev = repo.require_rev(&commit_chunk_ref)?;
1308 let mut chunking = chunking::Chunking::new(repo, &commit_chunk_rev)?;
1309 for layer in component_layers {
1310 chunking_from_layer_committed(repo, layer, &mut chunking)?;
1311 }
1312 let mut new_manifest = srcinfo.manifest.clone();
1316 new_manifest.layers_mut().clear();
1317 let mut new_config = srcinfo.configuration.clone();
1318 new_config.history_mut().clear();
1319
1320 let mut dest_oci = ocidir::OciDir::ensure(dest_oci)?;
1321
1322 let opts = ExportOpts {
1323 skip_compression: opts.skip_compression,
1324 authfile: opts.authfile,
1325 ..Default::default()
1326 };
1327
1328 let mut labels = HashMap::new();
1329
1330 export_chunked(
1333 repo,
1334 &srcinfo.base_commit,
1335 &mut dest_oci,
1336 &mut new_manifest,
1337 &mut new_config,
1338 &mut labels,
1339 chunking,
1340 &opts,
1341 "",
1342 )?;
1343
1344 let compression = opts.skip_compression.then_some(Compression::none());
1347 for (i, layer) in remaining_layers.iter().enumerate() {
1348 let layer_ref = &ref_for_layer(layer)?;
1349 let mut target_blob = dest_oci.create_gzip_layer(compression)?;
1350 let repo_dfd = repo.dfd_borrow();
1353 let repo_dir = cap_std_ext::cap_std::fs::Dir::reopen_dir(&repo_dfd)?;
1354 let mut subproc = std::process::Command::new("ostree")
1355 .args(["--repo=.", "export", layer_ref.as_str()])
1356 .stdout(std::process::Stdio::piped())
1357 .cwd_dir(repo_dir)
1358 .spawn()?;
1359 let mut stdout = subproc.stdout.take().unwrap();
1361 std::io::copy(&mut stdout, &mut target_blob).context("Creating blob")?;
1362 let layer = target_blob.complete()?;
1363 let previous_annotations = srcinfo
1364 .manifest
1365 .layers()
1366 .get(i)
1367 .and_then(|l| l.annotations().as_ref())
1368 .cloned();
1369 let previous_description = srcinfo
1370 .configuration
1371 .history()
1372 .get(i)
1373 .and_then(|h| h.comment().as_deref())
1374 .unwrap_or_default();
1375 dest_oci.push_layer(
1376 &mut new_manifest,
1377 &mut new_config,
1378 layer,
1379 previous_description,
1380 previous_annotations,
1381 )
1382 }
1383
1384 let new_config = dest_oci.write_config(new_config)?;
1385 new_manifest.set_config(new_config);
1386
1387 Ok(dest_oci.insert_manifest(new_manifest, tag, oci_image::Platform::default())?)
1388}
1389
1390#[context("Export")]
1393pub async fn export(
1394 repo: &ostree::Repo,
1395 src_imgref: &ImageReference,
1396 dest_imgref: &ImageReference,
1397 opts: Option<ExportToOCIOpts>,
1398) -> Result<oci_image::Digest> {
1399 let opts = opts.unwrap_or_default();
1400 let target_oci = dest_imgref.transport == Transport::OciDir;
1401 let tempdir = if !target_oci {
1402 let vartmp = cap_std::fs::Dir::open_ambient_dir("/var/tmp", cap_std::ambient_authority())?;
1403 let td = cap_std_ext::cap_tempfile::TempDir::new_in(&vartmp)?;
1404 let opts = ExportToOCIOpts {
1406 skip_compression: true,
1407 progress_to_stdout: opts.progress_to_stdout,
1408 ..Default::default()
1409 };
1410 export_to_oci(repo, src_imgref, &td, None, opts)?;
1411 td
1412 } else {
1413 let (path, tag) = parse_oci_path_and_tag(dest_imgref.name.as_str());
1414 tracing::debug!("using OCI path={path} tag={tag:?}");
1415 let path = Dir::open_ambient_dir(path, cap_std::ambient_authority())
1416 .with_context(|| format!("Opening {path}"))?;
1417 let descriptor = export_to_oci(repo, src_imgref, &path, tag, opts)?;
1418 return Ok(descriptor.digest().clone());
1419 };
1420 let target_fd = 3i32;
1422 let tempoci = ImageReference {
1423 transport: Transport::OciDir,
1424 name: format!("/proc/self/fd/{target_fd}"),
1425 };
1426 let authfile = opts.authfile.as_deref();
1427 skopeo::copy(
1428 &tempoci,
1429 dest_imgref,
1430 authfile,
1431 Some((std::sync::Arc::new(tempdir.try_clone()?.into()), target_fd)),
1432 opts.progress_to_stdout,
1433 )
1434 .await
1435}
1436
1437#[context("Listing deployment manifests")]
1440fn list_container_deployment_manifests(
1441 repo: &ostree::Repo,
1442 cancellable: Option<&gio::Cancellable>,
1443) -> Result<Vec<ImageManifest>> {
1444 let commits = OSTREE_BASE_DEPLOYMENT_REFS
1447 .iter()
1448 .chain(RPMOSTREE_BASE_REFS)
1449 .chain(std::iter::once(&BASE_IMAGE_PREFIX))
1450 .try_fold(
1451 std::collections::HashSet::new(),
1452 |mut acc, &p| -> Result<_> {
1453 let refs = repo.list_refs_ext(
1454 Some(p),
1455 ostree::RepoListRefsExtFlags::empty(),
1456 cancellable,
1457 )?;
1458 for (_, v) in refs {
1459 acc.insert(v);
1460 }
1461 Ok(acc)
1462 },
1463 )?;
1464 let mut r = Vec::new();
1466 for commit in commits {
1467 let commit_obj = repo.load_commit(&commit)?.0;
1468 let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
1469 if commit_meta
1470 .lookup::<String>(META_MANIFEST_DIGEST)?
1471 .is_some()
1472 {
1473 tracing::trace!("Commit {commit} is a container image");
1474 let manifest = manifest_data_from_commitmeta(commit_meta)?.0;
1475 r.push(manifest);
1476 }
1477 }
1478 Ok(r)
1479}
1480
1481pub fn gc_image_layers(repo: &ostree::Repo) -> Result<u32> {
1487 gc_image_layers_impl(repo, gio::Cancellable::NONE)
1488}
1489
1490#[context("Pruning image layers")]
1491fn gc_image_layers_impl(
1492 repo: &ostree::Repo,
1493 cancellable: Option<&gio::Cancellable>,
1494) -> Result<u32> {
1495 let all_images = list_images(repo)?;
1496 let deployment_commits = list_container_deployment_manifests(repo, cancellable)?;
1497 let all_manifests = all_images
1498 .into_iter()
1499 .map(|img| {
1500 ImageReference::try_from(img.as_str()).and_then(|ir| manifest_for_image(repo, &ir))
1501 })
1502 .chain(deployment_commits.into_iter().map(Ok))
1503 .collect::<Result<Vec<_>>>()?;
1504 tracing::debug!("Images found: {}", all_manifests.len());
1505 let mut referenced_layers = BTreeSet::new();
1506 for m in all_manifests.iter() {
1507 for layer in m.layers() {
1508 referenced_layers.insert(layer.digest().to_string());
1509 }
1510 }
1511 tracing::debug!("Referenced layers: {}", referenced_layers.len());
1512 let found_layers = repo
1513 .list_refs_ext(
1514 Some(LAYER_PREFIX),
1515 ostree::RepoListRefsExtFlags::empty(),
1516 cancellable,
1517 )?
1518 .into_iter()
1519 .map(|v| v.0);
1520 tracing::debug!("Found layers: {}", found_layers.len());
1521 let mut pruned = 0u32;
1522 for layer_ref in found_layers {
1523 let layer_digest = refescape::unprefix_unescape_ref(LAYER_PREFIX, &layer_ref)?;
1524 if referenced_layers.remove(layer_digest.as_str()) {
1525 continue;
1526 }
1527 pruned += 1;
1528 tracing::debug!("Pruning: {}", layer_ref.as_str());
1529 repo.set_ref_immediate(None, layer_ref.as_str(), None, cancellable)?;
1530 }
1531
1532 Ok(pruned)
1533}
1534
1535#[cfg(feature = "internal-testing-api")]
1536pub fn count_layer_references(repo: &ostree::Repo) -> Result<u32> {
1538 let cancellable = gio::Cancellable::NONE;
1539 let n = repo
1540 .list_refs_ext(
1541 Some(LAYER_PREFIX),
1542 ostree::RepoListRefsExtFlags::empty(),
1543 cancellable,
1544 )?
1545 .len();
1546 Ok(n as u32)
1547}
1548
1549pub fn image_filtered_content_warning(
1552 repo: &ostree::Repo,
1553 image: &ImageReference,
1554) -> Result<Option<String>> {
1555 use std::fmt::Write;
1556
1557 let ostree_ref = ref_for_image(image)?;
1558 let rev = repo.require_rev(&ostree_ref)?;
1559 let commit_obj = repo.load_commit(rev.as_str())?.0;
1560 let commit_meta = &glib::VariantDict::new(Some(&commit_obj.child_value(0)));
1561
1562 let r = commit_meta
1563 .lookup::<MetaFilteredData>(META_FILTERED)?
1564 .filter(|v| !v.is_empty())
1565 .map(|v| {
1566 let mut filtered = HashMap::<&String, u32>::new();
1567 for paths in v.values() {
1568 for (k, v) in paths {
1569 let e = filtered.entry(k).or_default();
1570 *e += v;
1571 }
1572 }
1573 let mut buf = "Image contains non-ostree compatible file paths:".to_string();
1574 for (k, v) in filtered {
1575 write!(buf, " {k}: {v}").unwrap();
1576 }
1577 buf
1578 });
1579 Ok(r)
1580}
1581
1582#[context("Pruning {img}")]
1589pub fn remove_image(repo: &ostree::Repo, img: &ImageReference) -> Result<bool> {
1590 let ostree_ref = &ref_for_image(img)?;
1591 let found = repo.resolve_rev(ostree_ref, true)?.is_some();
1592 if found {
1595 repo.set_ref_immediate(None, ostree_ref, None, gio::Cancellable::NONE)?;
1596 }
1597 Ok(found)
1598}
1599
1600pub fn remove_images<'a>(
1607 repo: &ostree::Repo,
1608 imgs: impl IntoIterator<Item = &'a ImageReference>,
1609) -> Result<()> {
1610 let mut missing = Vec::new();
1611 for img in imgs.into_iter() {
1612 let found = remove_image(repo, img)?;
1613 if !found {
1614 missing.push(img);
1615 }
1616 }
1617 if !missing.is_empty() {
1618 let missing = missing.into_iter().fold("".to_string(), |mut a, v| {
1619 a.push_str(&v.to_string());
1620 a
1621 });
1622 return Err(anyhow::anyhow!("Missing images: {missing}"));
1623 }
1624 Ok(())
1625}
1626
1627#[derive(Debug, Default)]
1628struct CompareState {
1629 verified: BTreeSet<Utf8PathBuf>,
1630 inode_corrupted: BTreeSet<Utf8PathBuf>,
1631 unknown_corrupted: BTreeSet<Utf8PathBuf>,
1632}
1633
1634impl CompareState {
1635 fn is_ok(&self) -> bool {
1636 self.inode_corrupted.is_empty() && self.unknown_corrupted.is_empty()
1637 }
1638}
1639
1640fn compare_file_info(src: &gio::FileInfo, target: &gio::FileInfo) -> bool {
1641 if src.file_type() != target.file_type() {
1642 return false;
1643 }
1644 if src.size() != target.size() {
1645 return false;
1646 }
1647 for attr in ["unix::uid", "unix::gid", "unix::mode"] {
1648 if src.attribute_uint32(attr) != target.attribute_uint32(attr) {
1649 return false;
1650 }
1651 }
1652 true
1653}
1654
1655#[context("Querying object inode")]
1656fn inode_of_object(repo: &ostree::Repo, checksum: &str) -> Result<u64> {
1657 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
1658 let (prefix, suffix) = checksum.split_at(2);
1659 let objpath = format!("objects/{}/{}.file", prefix, suffix);
1660 let metadata = repodir.symlink_metadata(objpath)?;
1661 Ok(metadata.ino())
1662}
1663
1664fn compare_commit_trees(
1665 repo: &ostree::Repo,
1666 root: &Utf8Path,
1667 target: &ostree::RepoFile,
1668 expected: &ostree::RepoFile,
1669 exact: bool,
1670 colliding_inodes: &BTreeSet<u64>,
1671 state: &mut CompareState,
1672) -> Result<()> {
1673 let cancellable = gio::Cancellable::NONE;
1674 let queryattrs = "standard::name,standard::type";
1675 let queryflags = gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS;
1676 let expected_iter = expected.enumerate_children(queryattrs, queryflags, cancellable)?;
1677
1678 while let Some(expected_info) = expected_iter.next_file(cancellable)? {
1679 let expected_child = expected_iter.child(&expected_info);
1680 let name = expected_info.name();
1681 let name = name.to_str().expect("UTF-8 ostree name");
1682 let path = Utf8PathBuf::from(format!("{root}{name}"));
1683 let target_child = target.child(name);
1684 let target_info = crate::diff::query_info_optional(&target_child, queryattrs, queryflags)
1685 .context("querying optional to")?;
1686 let is_dir = matches!(expected_info.file_type(), gio::FileType::Directory);
1687 if let Some(target_info) = target_info {
1688 let to_child = target_child
1689 .downcast::<ostree::RepoFile>()
1690 .expect("downcast");
1691 to_child.ensure_resolved()?;
1692 let from_child = expected_child
1693 .downcast::<ostree::RepoFile>()
1694 .expect("downcast");
1695 from_child.ensure_resolved()?;
1696
1697 if is_dir {
1698 let from_contents_checksum = from_child.tree_get_contents_checksum();
1699 let to_contents_checksum = to_child.tree_get_contents_checksum();
1700 if from_contents_checksum != to_contents_checksum {
1701 let subpath = Utf8PathBuf::from(format!("{}/", path));
1702 compare_commit_trees(
1703 repo,
1704 &subpath,
1705 &from_child,
1706 &to_child,
1707 exact,
1708 colliding_inodes,
1709 state,
1710 )?;
1711 }
1712 } else {
1713 let from_checksum = from_child.checksum();
1714 let to_checksum = to_child.checksum();
1715 let matches = if exact {
1716 from_checksum == to_checksum
1717 } else {
1718 compare_file_info(&target_info, &expected_info)
1719 };
1720 if !matches {
1721 let from_inode = inode_of_object(repo, &from_checksum)?;
1722 let to_inode = inode_of_object(repo, &to_checksum)?;
1723 if colliding_inodes.contains(&from_inode)
1724 || colliding_inodes.contains(&to_inode)
1725 {
1726 state.inode_corrupted.insert(path);
1727 } else {
1728 state.unknown_corrupted.insert(path);
1729 }
1730 } else {
1731 state.verified.insert(path);
1732 }
1733 }
1734 } else {
1735 eprintln!("Missing {path}");
1736 state.unknown_corrupted.insert(path);
1737 }
1738 }
1739 Ok(())
1740}
1741
1742#[context("Verifying container image state")]
1743pub(crate) fn verify_container_image(
1744 sysroot: &SysrootLock,
1745 imgref: &ImageReference,
1746 state: &LayeredImageState,
1747 colliding_inodes: &BTreeSet<u64>,
1748 verbose: bool,
1749) -> Result<bool> {
1750 let cancellable = gio::Cancellable::NONE;
1751 let repo = &sysroot.repo();
1752 let merge_commit = state.merge_commit.as_str();
1753 let merge_commit_root = repo.read_commit(merge_commit, gio::Cancellable::NONE)?.0;
1754 let merge_commit_root = merge_commit_root
1755 .downcast::<ostree::RepoFile>()
1756 .expect("downcast");
1757 merge_commit_root.ensure_resolved()?;
1758
1759 let (commit_layer, _component_layers, remaining_layers) =
1760 parse_manifest_layout(&state.manifest, &state.configuration)?;
1761
1762 let mut comparison_state = CompareState::default();
1763
1764 let query = |l: &Descriptor| query_layer(repo, l.clone());
1765
1766 let base_tree = repo
1767 .read_commit(&state.base_commit, cancellable)?
1768 .0
1769 .downcast::<ostree::RepoFile>()
1770 .expect("downcast");
1771 println!(
1772 "Verifying with base ostree layer {}",
1773 ref_for_layer(commit_layer)?
1774 );
1775 compare_commit_trees(
1776 repo,
1777 "/".into(),
1778 &merge_commit_root,
1779 &base_tree,
1780 true,
1781 colliding_inodes,
1782 &mut comparison_state,
1783 )?;
1784
1785 let remaining_layers = remaining_layers
1786 .into_iter()
1787 .map(query)
1788 .collect::<Result<Vec<_>>>()?;
1789
1790 println!("Image has {} derived layers", remaining_layers.len());
1791
1792 for layer in remaining_layers.iter().rev() {
1793 let layer_ref = layer.ostree_ref.as_str();
1794 let layer_commit = layer
1795 .commit
1796 .as_deref()
1797 .ok_or_else(|| anyhow!("Missing layer {layer_ref}"))?;
1798 let layer_tree = repo
1799 .read_commit(layer_commit, cancellable)?
1800 .0
1801 .downcast::<ostree::RepoFile>()
1802 .expect("downcast");
1803 compare_commit_trees(
1804 repo,
1805 "/".into(),
1806 &merge_commit_root,
1807 &layer_tree,
1808 false,
1809 colliding_inodes,
1810 &mut comparison_state,
1811 )?;
1812 }
1813
1814 let n_verified = comparison_state.verified.len();
1815 if comparison_state.is_ok() {
1816 println!("OK image {imgref} (verified={n_verified})");
1817 println!();
1818 } else {
1819 let n_inode = comparison_state.inode_corrupted.len();
1820 let n_other = comparison_state.unknown_corrupted.len();
1821 eprintln!("warning: Found corrupted merge commit");
1822 eprintln!(" inode clashes: {n_inode}");
1823 eprintln!(" unknown: {n_other}");
1824 eprintln!(" ok: {n_verified}");
1825 if verbose {
1826 eprintln!("Mismatches:");
1827 for path in comparison_state.inode_corrupted {
1828 eprintln!(" inode: {path}");
1829 }
1830 for path in comparison_state.unknown_corrupted {
1831 eprintln!(" other: {path}");
1832 }
1833 }
1834 eprintln!();
1835 return Ok(false);
1836 }
1837
1838 Ok(true)
1839}
1840
1841#[cfg(test)]
1842mod tests {
1843 use oci_image::{DescriptorBuilder, MediaType, Sha256Digest};
1844
1845 use super::*;
1846
1847 #[test]
1848 fn test_ref_for_descriptor() {
1849 let d = DescriptorBuilder::default()
1850 .size(42u64)
1851 .media_type(MediaType::ImageManifest)
1852 .digest(
1853 Sha256Digest::from_str(
1854 "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
1855 )
1856 .unwrap(),
1857 )
1858 .build()
1859 .unwrap();
1860 assert_eq!(ref_for_layer(&d).unwrap(), "ostree/container/blob/sha256_3A_2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae");
1861 }
1862}