Skip to content

Commit 10b66fb

Browse files
committed
feat(switch): support tag + digest image reference
Closes #1165 Performs tag-stripping when the image reference contains both a tag and digest. This allows Skopeo to pull the image successfully, while still displaying both the tag + digest inside bootc status. Signed-off-by: Robert Sturla <[email protected]>
1 parent 93b22f4 commit 10b66fb

File tree

2 files changed

+81
-3
lines changed

2 files changed

+81
-3
lines changed

lib/src/deploy.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,9 @@ pub(crate) async fn prepare_for_pull(
340340
imgref: &ImageReference,
341341
target_imgref: Option<&OstreeImageReference>,
342342
) -> Result<PreparedPullResult> {
343-
let ostree_imgref = &OstreeImageReference::from(imgref.clone());
343+
let imgref_canonicalized = imgref.clone().canonicalize()?;
344+
tracing::debug!("Canonicalized image reference: {imgref_canonicalized:#}");
345+
let ostree_imgref = &OstreeImageReference::from(imgref_canonicalized);
344346
let mut imp = new_importer(repo, ostree_imgref).await?;
345347
if let Some(target) = target_imgref {
346348
imp.set_target(target);
@@ -420,7 +422,9 @@ pub(crate) async fn pull_from_prepared(
420422
})
421423
.await;
422424
let import = import?;
423-
let ostree_imgref = &OstreeImageReference::from(imgref.clone());
425+
let imgref_canonicalized = imgref.clone().canonicalize()?;
426+
tracing::debug!("Canonicalized image reference: {imgref_canonicalized:#}");
427+
let ostree_imgref = &OstreeImageReference::from(imgref_canonicalized);
424428
let wrote_imgref = target_imgref.as_ref().unwrap_or(&ostree_imgref);
425429

426430
if let Some(msg) =

lib/src/spec.rs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
33
use std::fmt::Display;
44

5-
use ostree_ext::container::OstreeImageReference;
65
use ostree_ext::oci_spec::image::Digest;
6+
use ostree_ext::{container::OstreeImageReference, oci_spec};
77
use schemars::JsonSchema;
88
use serde::{Deserialize, Serialize};
99

@@ -89,6 +89,25 @@ pub struct ImageReference {
8989
pub signature: Option<ImageSignature>,
9090
}
9191

92+
impl ImageReference {
93+
/// Returns a canonicalized version of this image reference, preferring the digest over the tag if both are present.
94+
pub fn canonicalize(self) -> Result<Self, anyhow::Error> {
95+
let reference: oci_spec::distribution::Reference = self.image.parse()?;
96+
97+
if reference.digest().is_some() && reference.tag().is_some() {
98+
let registry = reference.registry();
99+
let repository = reference.repository();
100+
let digest = reference.digest().expect("digest is present");
101+
return Ok(ImageReference {
102+
image: format!("{registry}/{repository}@{digest}"),
103+
..self
104+
});
105+
}
106+
107+
Ok(self)
108+
}
109+
}
110+
92111
/// The status of the booted image
93112
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
94113
#[serde(rename_all = "camelCase")]
@@ -253,6 +272,61 @@ mod tests {
253272

254273
use super::*;
255274

275+
#[test]
276+
fn test_image_reference_canonicalize() {
277+
let sample_digest =
278+
"sha256:5db6d8b5f34d3cbdaa1e82ed0152a5ac980076d19317d4269db149cbde057bb2";
279+
let test_cases = [
280+
// When both a tag and digest are present, the digest should be used
281+
(
282+
format!("quay.io/example/someimage:latest@{}", sample_digest),
283+
format!("quay.io/example/someimage@{}", sample_digest),
284+
),
285+
// When only a digest is present, it should be used
286+
(
287+
format!("quay.io/example/someimage@{}", sample_digest),
288+
format!("quay.io/example/someimage@{}", sample_digest),
289+
),
290+
// When only a tag is present, it should be preserved
291+
(
292+
"quay.io/example/someimage:latest".to_string(),
293+
"quay.io/example/someimage:latest".to_string(),
294+
),
295+
// When no tag or digest is present, preserve the original image name
296+
(
297+
"quay.io/example/someimage".to_string(),
298+
"quay.io/example/someimage".to_string(),
299+
),
300+
// When used with a local image (i.e. from containers-storage), the functionality should
301+
// be the same as previous cases
302+
(
303+
"localhost/someimage:latest".to_string(),
304+
"localhost/someimage:latest".to_string(),
305+
),
306+
(
307+
format!("localhost/someimage:latest@{sample_digest}"),
308+
format!("localhost/someimage@{sample_digest}"),
309+
),
310+
];
311+
312+
for (initial, expected) in test_cases {
313+
let imgref = ImageReference {
314+
image: initial.to_string(),
315+
transport: "registry".to_string(),
316+
signature: None,
317+
};
318+
319+
let canonicalized = imgref.canonicalize();
320+
if let Err(e) = canonicalized {
321+
panic!("Failed to canonicalize {initial}: {e}");
322+
}
323+
let canonicalized = canonicalized.unwrap();
324+
assert_eq!(canonicalized.image, expected);
325+
assert_eq!(canonicalized.transport, "registry");
326+
assert_eq!(canonicalized.signature, None);
327+
}
328+
}
329+
256330
#[test]
257331
fn test_parse_spec_v1_null() {
258332
const SPEC_FIXTURE: &str = include_str!("fixtures/spec-v1-null.json");

0 commit comments

Comments
 (0)