From 8da555767c6becaa8303d285a745fc42f2f08678 Mon Sep 17 00:00:00 2001 From: Jonathan Lebon Date: Thu, 19 Jun 2025 12:56:23 -0400 Subject: [PATCH] store: short-circuit pull if digest pullspec already exists If we're pulling by digest and the pullspec already exists, then there's no need to reach out to the registry or even spawn skopeo. Detect this case and exit early in the pull code. This allows RHCOS to conform better to the PinnedImageSet API[1], where the expectation is that once an image is pulled, the registry will not be contacted again. In a future with unified storage, the MCO's pre-pull would work just the same for the RHCOS image as any other. Framing this more generally: this patch allows one to pre-pull an image into the store, and making the later deployment operation be fully offline. E.g. this could be used to implement a `bootc switch --download-only` option. [1] https://github.com/openshift/enhancements/blob/26ce3cd8a0c7ce650e73bc5393a3605022cb6847/enhancements/machine-config/pin-and-pre-load-images.md Signed-off-by: Colin Walters Co-authored-by: Colin Walters Signed-off-by: Colin Walters --- ostree-ext/src/container/store.rs | 36 +++++++++++++++++++++ ostree-ext/tests/it/main.rs | 54 +++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/ostree-ext/src/container/store.rs b/ostree-ext/src/container/store.rs index b9824e2f..f02ace71 100644 --- a/ostree-ext/src/container/store.rs +++ b/ostree-ext/src/container/store.rs @@ -26,6 +26,7 @@ use glib::prelude::*; use oci_spec::image::{ self as oci_image, Arch, Descriptor, Digest, History, ImageConfiguration, ImageManifest, }; +use ocidir::oci_spec::distribution::Reference; use ostree::prelude::{Cast, FileEnumeratorExt, FileExt, ToVariant}; use ostree::{gio, glib}; use std::collections::{BTreeMap, BTreeSet, HashMap}; @@ -181,6 +182,8 @@ pub struct ImageImporter { disable_gc: bool, // If true, don't prune unused image layers /// If true, require the image has the bootable flag require_bootable: bool, + /// Do not attempt to contact the network + offline: bool, /// If true, we have ostree v2024.3 or newer. ostree_v2024_3: bool, @@ -519,6 +522,7 @@ impl ImageImporter { ostree_v2024_3: ostree::check_version(2024, 3), disable_gc: false, require_bootable: false, + offline: false, imgref: imgref.clone(), layer_progress: None, layer_byte_progress: None, @@ -537,6 +541,11 @@ impl ImageImporter { self.no_imgref = true; } + /// Do not attempt to contact the network + pub fn set_offline(&mut self) { + self.offline = true; + } + /// Require that the image has the bootable metadata field pub fn require_bootable(&mut self) { self.require_bootable = true; @@ -682,8 +691,35 @@ impl ImageImporter { _ => {} } + // Check if we have an image already pulled let previous_state = try_query_image(&self.repo, &self.imgref.imgref)?; + // Parse the target reference to see if it's a digested pull + let target_reference = self.imgref.imgref.name.parse::().ok(); + let previous_state = if let Some(target_digest) = target_reference + .as_ref() + .and_then(|v| v.digest()) + .map(Digest::from_str) + .transpose()? + { + if let Some(previous_state) = previous_state { + // A digested pull spec, and our existing state matches. + if previous_state.manifest_digest == target_digest { + tracing::debug!("Digest-based pullspec {:?} already present", self.imgref); + return Ok(PrepareResult::AlreadyPresent(previous_state)); + } + Some(previous_state) + } else { + None + } + } else { + previous_state + }; + + if self.offline { + anyhow::bail!("Manifest fetch required in offline mode"); + } + let proxy_img = self .proxy .open_image(&self.imgref.imgref.to_string()) diff --git a/ostree-ext/tests/it/main.rs b/ostree-ext/tests/it/main.rs index cfa1e1a8..004aabba 100644 --- a/ostree-ext/tests/it/main.rs +++ b/ostree-ext/tests/it/main.rs @@ -9,6 +9,7 @@ use gvariant::aligned_bytes::TryAsAligned; use gvariant::{Marker, Structure}; use oci_image::ImageManifest; use oci_spec::image as oci_image; +use ocidir::oci_spec::distribution::Reference; use ocidir::oci_spec::image::{Arch, DigestAlgorithm}; use ostree_ext::chunking::ObjectMetaSized; use ostree_ext::container::{store, ManifestDiff}; @@ -712,6 +713,59 @@ async fn test_export_as_container_nonderived() -> Result<()> { Ok(()) } +/// Verify that fetches of a digested pull spec don't do networking +#[tokio::test] +async fn test_no_fetch_digested() -> Result<()> { + if !check_skopeo() { + return Ok(()); + } + let fixture = Fixture::new_v1()?; + let (src_imgref_oci, expected_digest) = fixture.export_container().await.unwrap(); + let mut imp = store::ImageImporter::new( + fixture.destrepo(), + &OstreeImageReference { + sigverify: SignatureSource::ContainerPolicyAllowInsecure, + imgref: src_imgref_oci.clone(), + }, + Default::default(), + ) + .await + .unwrap(); + // Because oci: transport doesn't allow digested pull specs, we pull from OCI, but set the target + // to a registry. + let target_imgref_name = Reference::with_digest( + "quay.io/exampleos".into(), + "example".into(), + expected_digest.to_string(), + ); + let target_imgref = ImageReference { + transport: Transport::Registry, + name: target_imgref_name.to_string(), + }; + let target_imgref = OstreeImageReference { + sigverify: SignatureSource::ContainerPolicyAllowInsecure, + imgref: target_imgref, + }; + imp.set_target(&target_imgref); + let prep = match imp.prepare().await? { + store::PrepareResult::AlreadyPresent(_) => unreachable!(), + store::PrepareResult::Ready(prep) => prep, + }; + let r = imp.import(prep).await.unwrap(); + assert_eq!(r.manifest_digest, expected_digest); + let mut imp = store::ImageImporter::new(fixture.destrepo(), &target_imgref, Default::default()) + .await + .unwrap(); + // And the key test, we shouldn't reach out to the registry here + imp.set_offline(); + match imp.prepare().await.context("Init prep derived").unwrap() { + store::PrepareResult::AlreadyPresent(_) => {} + store::PrepareResult::Ready(_) => panic!("Should have image already"), + }; + + Ok(()) +} + #[tokio::test] async fn test_export_as_container_derived() -> Result<()> { if !check_skopeo() {