1
0
mirror of https://github.com/containers/bootc.git synced 2026-02-05 15:45:53 +01:00

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] 26ce3cd8a0/enhancements/machine-config/pin-and-pre-load-images.md

Signed-off-by: Colin Walters <walters@verbum.org>
Co-authored-by: Colin Walters <walters@verbum.org>
Signed-off-by: Colin Walters <walters@verbum.org>
This commit is contained in:
Jonathan Lebon
2025-06-19 12:56:23 -04:00
committed by Colin Walters
parent fff106ec62
commit 8da555767c
2 changed files with 90 additions and 0 deletions

View File

@@ -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::<Reference>().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())

View File

@@ -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() {