Skip to content

Commit de0eb23

Browse files
committed
spec: Note we don't canonicalize oci
The previous code in trying to parse `oci` was wrong; the syntax for an `oci` transport is the same as oci-archive, which includes a file path (and there's no mechanism to quote `:` note). The canonical logic for all of this stuff is in Go, there's no canonical Rust library (yet, though I did think about putting this in oci-spec). Previous to this attempt to handle tagged+digested, we weren't parsing image references at all. First, factor out a `canonicalize_reference` helper since that's what we're really doing here, it's independent of the *transport*. Fix the canonicalize function to drop out trying to parse `oci`. Add a separate test case that incorrectly passes just so it's a bit more obvious to fix this later. Note that today at least `skopeo` rejects trying to fetch via tagged+digested form from an `oci:` so it's fine if we don't canonicalize here yet, even though it could confuse someone. Signed-off-by: Colin Walters <[email protected]>
1 parent 21c57d4 commit de0eb23

File tree

1 file changed

+74
-18
lines changed

1 file changed

+74
-18
lines changed

lib/src/spec.rs

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
use std::fmt::Display;
44

55
use anyhow::Result;
6+
use ostree_ext::container::Transport;
7+
use ostree_ext::oci_spec::distribution::Reference;
68
use ostree_ext::oci_spec::image::Digest;
79
use ostree_ext::{container::OstreeImageReference, oci_spec};
810
use schemars::JsonSchema;
@@ -90,27 +92,37 @@ pub struct ImageReference {
9092
pub signature: Option<ImageSignature>,
9193
}
9294

95+
/// If the reference is in :tag@digest form, strip the tag.
96+
fn canonicalize_reference(reference: Reference) -> Option<Reference> {
97+
// No tag? Just pass through.
98+
if reference.tag().is_none() {
99+
return None;
100+
}
101+
102+
// No digest? Also pass through.
103+
let Some(digest) = reference.digest() else {
104+
return None;
105+
};
106+
107+
Some(reference.clone_with_digest(digest.to_owned()))
108+
}
109+
93110
impl ImageReference {
94111
/// Returns a canonicalized version of this image reference, preferring the digest over the tag if both are present.
95112
pub fn canonicalize(self) -> Result<Self> {
96-
match self.transport.as_str() {
97-
"containers-storage" | "registry" | "oci" => {
113+
// TODO maintain a proper transport enum in the spec here
114+
let transport = Transport::try_from(self.transport.as_str())?;
115+
match transport {
116+
Transport::ContainerStorage | Transport::Registry => {
98117
let reference: oci_spec::distribution::Reference = self.image.parse()?;
99118

100-
// No tag? Just pass through.
101-
if reference.tag().is_none() {
102-
return Ok(self);
103-
}
104-
105-
// No digest? Also pass through.
106-
let Some(digest) = reference.digest() else {
119+
// Check if the image reference needs canonicicalization
120+
let Some(reference) = canonicalize_reference(reference) else {
107121
return Ok(self);
108122
};
109123

110-
let registry = reference.registry();
111-
let repository = reference.repository();
112124
let r = ImageReference {
113-
image: format!("{registry}/{repository}@{digest}"),
125+
image: reference.to_string(),
114126
transport: self.transport.clone(),
115127
signature: self.signature.clone(),
116128
};
@@ -288,6 +300,38 @@ mod tests {
288300

289301
use super::*;
290302

303+
#[test]
304+
fn test_canonicalize_reference() {
305+
// expand this
306+
let passthrough = [
307+
("quay.io/example/someimage:latest"),
308+
("quay.io/example/someimage"),
309+
("quay.io/example/someimage@sha256:5db6d8b5f34d3cbdaa1e82ed0152a5ac980076d19317d4269db149cbde057bb2"),
310+
];
311+
let mapped = [
312+
(
313+
"quay.io/example/someimage:latest@sha256:5db6d8b5f34d3cbdaa1e82ed0152a5ac980076d19317d4269db149cbde057bb2",
314+
"quay.io/example/someimage@sha256:5db6d8b5f34d3cbdaa1e82ed0152a5ac980076d19317d4269db149cbde057bb2",
315+
),
316+
(
317+
"localhost/someimage:latest@sha256:5db6d8b5f34d3cbdaa1e82ed0152a5ac980076d19317d4269db149cbde057bb2",
318+
"localhost/someimage@sha256:5db6d8b5f34d3cbdaa1e82ed0152a5ac980076d19317d4269db149cbde057bb2",
319+
),
320+
];
321+
for &v in passthrough.iter() {
322+
let reference = Reference::from_str(v).unwrap();
323+
assert!(reference.tag().is_none() || reference.digest().is_none());
324+
assert!(canonicalize_reference(reference).is_none());
325+
}
326+
for &(initial, expected) in mapped.iter() {
327+
let reference = Reference::from_str(initial).unwrap();
328+
assert!(reference.tag().is_some());
329+
assert!(reference.digest().is_some());
330+
let canonicalized = canonicalize_reference(reference).unwrap();
331+
assert_eq!(canonicalized.to_string(), expected);
332+
}
333+
}
334+
291335
#[test]
292336
fn test_image_reference_canonicalize() {
293337
let sample_digest =
@@ -305,11 +349,6 @@ mod tests {
305349
format!("quay.io/example/someimage@{}", sample_digest),
306350
"containers-storage",
307351
),
308-
(
309-
format!("quay.io/example/someimage:latest@{}", sample_digest),
310-
format!("quay.io/example/someimage@{}", sample_digest),
311-
"oci",
312-
),
313352
// When only a digest is present, it should be used
314353
(
315354
format!("quay.io/example/someimage@{}", sample_digest),
@@ -340,7 +379,12 @@ mod tests {
340379
format!("localhost/someimage@{sample_digest}"),
341380
"registry",
342381
),
343-
// OCI Archive / Dir should be preserved, and canonicalize should be a no-op
382+
// Also for now, we do not canonicalize OCI references
383+
(
384+
format!("/path/to/dir:latest"),
385+
format!("/path/to/dir:latest"),
386+
"oci",
387+
),
344388
(
345389
"/tmp/repo".to_string(),
346390
"/tmp/repo".to_string(),
@@ -374,6 +418,18 @@ mod tests {
374418
}
375419
}
376420

421+
#[test]
422+
fn test_unimplemented_oci_tagged_digested() {
423+
let imgref = ImageReference {
424+
image: "path/to/image:sometag@sha256:5db6d8b5f34d3cbdaa1e82ed0152a5ac980076d19317d4269db149cbde057bb2".to_string(),
425+
transport: "oci".to_string(),
426+
signature: None
427+
};
428+
let canonicalized = imgref.clone().canonicalize().unwrap();
429+
// TODO For now this is known to incorrectly pass
430+
assert_eq!(imgref, canonicalized);
431+
}
432+
377433
#[test]
378434
fn test_parse_spec_v1_null() {
379435
const SPEC_FIXTURE: &str = include_str!("fixtures/spec-v1-null.json");

0 commit comments

Comments
 (0)