Skip to content

Commit 350fb40

Browse files
authored
Merge pull request #1380 from jlebon/pr/digest-pull
deploy: short-circuit pull if digest pullspec already exists
2 parents 9c72683 + 8da5557 commit 350fb40

File tree

2 files changed

+126
-29
lines changed

2 files changed

+126
-29
lines changed

ostree-ext/src/container/store.rs

Lines changed: 72 additions & 29 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,9 +182,10 @@ 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,
186-
pub(crate) proxy_img: OpenedImage,
187189

188190
layer_progress: Option<Sender<ImportProgress>>,
189191
layer_byte_progress: Option<tokio::sync::watch::Sender<Option<LayerProgress>>>,
@@ -241,6 +243,8 @@ pub struct PreparedImport {
241243
pub layers: Vec<ManifestLayerState>,
242244
/// OSTree remote signature verification text, if enabled.
243245
pub verify_text: Option<String>,
246+
/// Our open image reference
247+
proxy_img: OpenedImage,
244248
}
245249

246250
impl PreparedImport {
@@ -509,17 +513,16 @@ impl ImageImporter {
509513
&format!("Fetching {}", imgref),
510514
);
511515

512-
let proxy_img = proxy.open_image(&imgref.imgref.to_string()).await?;
513516
let repo = repo.clone();
514517
Ok(ImageImporter {
515518
repo,
516519
proxy,
517-
proxy_img,
518520
target_imgref: None,
519521
no_imgref: false,
520522
ostree_v2024_3: ostree::check_version(2024, 3),
521523
disable_gc: false,
522524
require_bootable: false,
525+
offline: false,
523526
imgref: imgref.clone(),
524527
layer_progress: None,
525528
layer_byte_progress: None,
@@ -538,6 +541,11 @@ impl ImageImporter {
538541
self.no_imgref = true;
539542
}
540543

544+
/// Do not attempt to contact the network
545+
pub fn set_offline(&mut self) {
546+
self.offline = true;
547+
}
548+
541549
/// Require that the image has the bootable metadata field
542550
pub fn require_bootable(&mut self) {
543551
self.require_bootable = true;
@@ -619,6 +627,7 @@ impl ImageImporter {
619627
config: ImageConfiguration,
620628
previous_state: Option<Box<LayeredImageState>>,
621629
previous_imageid: Option<String>,
630+
proxy_img: OpenedImage,
622631
) -> Result<Box<PreparedImport>> {
623632
let config_labels = super::labels_of(&config);
624633
if self.require_bootable {
@@ -662,6 +671,7 @@ impl ImageImporter {
662671
ostree_commit_layer: commit_layer,
663672
layers: remaining_layers,
664673
verify_text: None,
674+
proxy_img,
665675
};
666676
Ok(Box::new(imp))
667677
}
@@ -681,30 +691,63 @@ impl ImageImporter {
681691
_ => {}
682692
}
683693

684-
let (manifest_digest, manifest) = self.proxy.fetch_manifest(&self.proxy_img).await?;
694+
// Check if we have an image already pulled
695+
let previous_state = try_query_image(&self.repo, &self.imgref.imgref)?;
696+
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+
723+
let proxy_img = self
724+
.proxy
725+
.open_image(&self.imgref.imgref.to_string())
726+
.await?;
727+
728+
let (manifest_digest, manifest) = self.proxy.fetch_manifest(&proxy_img).await?;
685729
let manifest_digest = Digest::from_str(&manifest_digest)?;
686730
let new_imageid = manifest.config().digest();
687731

688732
// Query for previous stored state
689733

690-
let (previous_state, previous_imageid) =
691-
if let Some(previous_state) = try_query_image(&self.repo, &self.imgref.imgref)? {
692-
// If the manifest digests match, we're done.
693-
if previous_state.manifest_digest == manifest_digest {
694-
return Ok(PrepareResult::AlreadyPresent(previous_state));
695-
}
696-
// Failing that, if they have the same imageID, we're also done.
697-
let previous_imageid = previous_state.manifest.config().digest();
698-
if previous_imageid == new_imageid {
699-
return Ok(PrepareResult::AlreadyPresent(previous_state));
700-
}
701-
let previous_imageid = previous_imageid.to_string();
702-
(Some(previous_state), Some(previous_imageid))
703-
} else {
704-
(None, None)
705-
};
734+
let (previous_state, previous_imageid) = if let Some(previous_state) = previous_state {
735+
// If the manifest digests match, we're done.
736+
if previous_state.manifest_digest == manifest_digest {
737+
return Ok(PrepareResult::AlreadyPresent(previous_state));
738+
}
739+
// Failing that, if they have the same imageID, we're also done.
740+
let previous_imageid = previous_state.manifest.config().digest();
741+
if previous_imageid == new_imageid {
742+
return Ok(PrepareResult::AlreadyPresent(previous_state));
743+
}
744+
let previous_imageid = previous_imageid.to_string();
745+
(Some(previous_state), Some(previous_imageid))
746+
} else {
747+
(None, None)
748+
};
706749

707-
let config = self.proxy.fetch_config(&self.proxy_img).await?;
750+
let config = self.proxy.fetch_config(&proxy_img).await?;
708751

709752
// If there is a currently fetched image, cache the new pending manifest+config
710753
// as detached commit metadata, so that future fetches can query it offline.
@@ -724,6 +767,7 @@ impl ImageImporter {
724767
config,
725768
previous_state,
726769
previous_imageid,
770+
proxy_img,
727771
)?;
728772
Ok(PrepareResult::Ready(imp))
729773
}
@@ -756,7 +800,7 @@ impl ImageImporter {
756800
}
757801
return Ok(());
758802
};
759-
let des_layers = self.proxy.get_layer_info(&self.proxy_img).await?;
803+
let des_layers = self.proxy.get_layer_info(&import.proxy_img).await?;
760804
for layer in import.ostree_layers.iter_mut() {
761805
if layer.commit.is_some() {
762806
continue;
@@ -767,7 +811,7 @@ impl ImageImporter {
767811
}
768812
let (blob, driver, media_type) = fetch_layer(
769813
&self.proxy,
770-
&self.proxy_img,
814+
&import.proxy_img,
771815
&import.manifest,
772816
&layer.layer,
773817
self.layer_byte_progress.as_ref(),
@@ -814,7 +858,7 @@ impl ImageImporter {
814858
}
815859
let (blob, driver, media_type) = fetch_layer(
816860
&self.proxy,
817-
&self.proxy_img,
861+
&import.proxy_img,
818862
&import.manifest,
819863
&commit_layer.layer,
820864
self.layer_byte_progress.as_ref(),
@@ -874,7 +918,7 @@ impl ImageImporter {
874918
self.unencapsulate_base(&mut prep, true, false).await?;
875919
// TODO change the imageproxy API to ensure this happens automatically when
876920
// the image reference is dropped
877-
self.proxy.close_image(&self.proxy_img).await?;
921+
self.proxy.close_image(&prep.proxy_img).await?;
878922
// SAFETY: We know we have a commit
879923
let ostree_commit = prep.ostree_commit_layer.unwrap().commit.unwrap();
880924
let image_digest = prep.manifest_digest;
@@ -899,9 +943,8 @@ impl ImageImporter {
899943
// First download all layers for the base image (if necessary) - we need the SELinux policy
900944
// there to label all following layers.
901945
self.unencapsulate_base(&mut import, false, true).await?;
902-
let des_layers = self.proxy.get_layer_info(&self.proxy_img).await?;
946+
let des_layers = self.proxy.get_layer_info(&import.proxy_img).await?;
903947
let proxy = self.proxy;
904-
let proxy_img = self.proxy_img;
905948
let target_imgref = self.target_imgref.as_ref().unwrap_or(&self.imgref);
906949
let base_commit = import
907950
.ostree_commit_layer
@@ -935,7 +978,7 @@ impl ImageImporter {
935978
}
936979
let (blob, driver, media_type) = super::unencapsulate::fetch_layer(
937980
&proxy,
938-
&proxy_img,
981+
&import.proxy_img,
939982
&import.manifest,
940983
&layer.layer,
941984
self.layer_byte_progress.as_ref(),
@@ -989,7 +1032,7 @@ impl ImageImporter {
9891032

9901033
// TODO change the imageproxy API to ensure this happens automatically when
9911034
// the image reference is dropped
992-
proxy.close_image(&proxy_img).await?;
1035+
proxy.close_image(&import.proxy_img).await?;
9931036

9941037
// We're done with the proxy, make sure it didn't have any errors.
9951038
proxy.finalize().await?;

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)