Skip to content

Commit 8da5557

Browse files
jleboncgwalters
andcommitted
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 <[email protected]> Co-authored-by: Colin Walters <[email protected]> Signed-off-by: Colin Walters <[email protected]>
1 parent fff106e commit 8da5557

File tree

2 files changed

+90
-0
lines changed

2 files changed

+90
-0
lines changed

ostree-ext/src/container/store.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use glib::prelude::*;
2626
use oci_spec::image::{
2727
self as oci_image, Arch, Descriptor, Digest, History, ImageConfiguration, ImageManifest,
2828
};
29+
use ocidir::oci_spec::distribution::Reference;
2930
use ostree::prelude::{Cast, FileEnumeratorExt, FileExt, ToVariant};
3031
use ostree::{gio, glib};
3132
use std::collections::{BTreeMap, BTreeSet, HashMap};
@@ -181,6 +182,8 @@ pub struct ImageImporter {
181182
disable_gc: bool, // If true, don't prune unused image layers
182183
/// If true, require the image has the bootable flag
183184
require_bootable: bool,
185+
/// Do not attempt to contact the network
186+
offline: bool,
184187
/// If true, we have ostree v2024.3 or newer.
185188
ostree_v2024_3: bool,
186189

@@ -519,6 +522,7 @@ impl ImageImporter {
519522
ostree_v2024_3: ostree::check_version(2024, 3),
520523
disable_gc: false,
521524
require_bootable: false,
525+
offline: false,
522526
imgref: imgref.clone(),
523527
layer_progress: None,
524528
layer_byte_progress: None,
@@ -537,6 +541,11 @@ impl ImageImporter {
537541
self.no_imgref = true;
538542
}
539543

544+
/// Do not attempt to contact the network
545+
pub fn set_offline(&mut self) {
546+
self.offline = true;
547+
}
548+
540549
/// Require that the image has the bootable metadata field
541550
pub fn require_bootable(&mut self) {
542551
self.require_bootable = true;
@@ -682,8 +691,35 @@ impl ImageImporter {
682691
_ => {}
683692
}
684693

694+
// Check if we have an image already pulled
685695
let previous_state = try_query_image(&self.repo, &self.imgref.imgref)?;
686696

697+
// Parse the target reference to see if it's a digested pull
698+
let target_reference = self.imgref.imgref.name.parse::<Reference>().ok();
699+
let previous_state = if let Some(target_digest) = target_reference
700+
.as_ref()
701+
.and_then(|v| v.digest())
702+
.map(Digest::from_str)
703+
.transpose()?
704+
{
705+
if let Some(previous_state) = previous_state {
706+
// A digested pull spec, and our existing state matches.
707+
if previous_state.manifest_digest == target_digest {
708+
tracing::debug!("Digest-based pullspec {:?} already present", self.imgref);
709+
return Ok(PrepareResult::AlreadyPresent(previous_state));
710+
}
711+
Some(previous_state)
712+
} else {
713+
None
714+
}
715+
} else {
716+
previous_state
717+
};
718+
719+
if self.offline {
720+
anyhow::bail!("Manifest fetch required in offline mode");
721+
}
722+
687723
let proxy_img = self
688724
.proxy
689725
.open_image(&self.imgref.imgref.to_string())

ostree-ext/tests/it/main.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use gvariant::aligned_bytes::TryAsAligned;
99
use gvariant::{Marker, Structure};
1010
use oci_image::ImageManifest;
1111
use oci_spec::image as oci_image;
12+
use ocidir::oci_spec::distribution::Reference;
1213
use ocidir::oci_spec::image::{Arch, DigestAlgorithm};
1314
use ostree_ext::chunking::ObjectMetaSized;
1415
use ostree_ext::container::{store, ManifestDiff};
@@ -712,6 +713,59 @@ async fn test_export_as_container_nonderived() -> Result<()> {
712713
Ok(())
713714
}
714715

716+
/// Verify that fetches of a digested pull spec don't do networking
717+
#[tokio::test]
718+
async fn test_no_fetch_digested() -> Result<()> {
719+
if !check_skopeo() {
720+
return Ok(());
721+
}
722+
let fixture = Fixture::new_v1()?;
723+
let (src_imgref_oci, expected_digest) = fixture.export_container().await.unwrap();
724+
let mut imp = store::ImageImporter::new(
725+
fixture.destrepo(),
726+
&OstreeImageReference {
727+
sigverify: SignatureSource::ContainerPolicyAllowInsecure,
728+
imgref: src_imgref_oci.clone(),
729+
},
730+
Default::default(),
731+
)
732+
.await
733+
.unwrap();
734+
// Because oci: transport doesn't allow digested pull specs, we pull from OCI, but set the target
735+
// to a registry.
736+
let target_imgref_name = Reference::with_digest(
737+
"quay.io/exampleos".into(),
738+
"example".into(),
739+
expected_digest.to_string(),
740+
);
741+
let target_imgref = ImageReference {
742+
transport: Transport::Registry,
743+
name: target_imgref_name.to_string(),
744+
};
745+
let target_imgref = OstreeImageReference {
746+
sigverify: SignatureSource::ContainerPolicyAllowInsecure,
747+
imgref: target_imgref,
748+
};
749+
imp.set_target(&target_imgref);
750+
let prep = match imp.prepare().await? {
751+
store::PrepareResult::AlreadyPresent(_) => unreachable!(),
752+
store::PrepareResult::Ready(prep) => prep,
753+
};
754+
let r = imp.import(prep).await.unwrap();
755+
assert_eq!(r.manifest_digest, expected_digest);
756+
let mut imp = store::ImageImporter::new(fixture.destrepo(), &target_imgref, Default::default())
757+
.await
758+
.unwrap();
759+
// And the key test, we shouldn't reach out to the registry here
760+
imp.set_offline();
761+
match imp.prepare().await.context("Init prep derived").unwrap() {
762+
store::PrepareResult::AlreadyPresent(_) => {}
763+
store::PrepareResult::Ready(_) => panic!("Should have image already"),
764+
};
765+
766+
Ok(())
767+
}
768+
715769
#[tokio::test]
716770
async fn test_export_as_container_derived() -> Result<()> {
717771
if !check_skopeo() {

0 commit comments

Comments
 (0)