33use std:: fmt:: Display ;
44
55use anyhow:: Result ;
6+ use ostree_ext:: container:: Transport ;
7+ use ostree_ext:: oci_spec:: distribution:: Reference ;
68use ostree_ext:: oci_spec:: image:: Digest ;
79use ostree_ext:: { container:: OstreeImageReference , oci_spec} ;
810use 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+
93110impl 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