|
2 | 2 |
|
3 | 3 | use std::fmt::Display;
|
4 | 4 |
|
| 5 | +use anyhow::Result; |
5 | 6 | use ostree_ext::oci_spec::image::Digest;
|
6 | 7 | use ostree_ext::{container::OstreeImageReference, oci_spec};
|
7 | 8 | use schemars::JsonSchema;
|
@@ -91,20 +92,35 @@ pub struct ImageReference {
|
91 | 92 |
|
92 | 93 | impl ImageReference {
|
93 | 94 | /// 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 |
| - }); |
| 95 | + pub fn canonicalize(self) -> Result<Self> { |
| 96 | + match self.transport.as_str() { |
| 97 | + "containers-storage" | "registry" | "oci" => { |
| 98 | + let reference: oci_spec::distribution::Reference = self.image.parse()?; |
| 99 | + |
| 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 { |
| 107 | + return Ok(self); |
| 108 | + }; |
| 109 | + |
| 110 | + let registry = reference.registry(); |
| 111 | + let repository = reference.repository(); |
| 112 | + let r = ImageReference { |
| 113 | + image: format!("{registry}/{repository}@{digest}"), |
| 114 | + transport: self.transport.clone(), |
| 115 | + signature: self.signature.clone(), |
| 116 | + }; |
| 117 | + return Ok(r); |
| 118 | + } |
| 119 | + _ => { |
| 120 | + // For other transports, we don't do any canonicalization |
| 121 | + Ok(self) |
| 122 | + } |
105 | 123 | }
|
106 |
| - |
107 |
| - Ok(self) |
108 | 124 | }
|
109 | 125 | }
|
110 | 126 |
|
@@ -276,53 +292,84 @@ mod tests {
|
276 | 292 | fn test_image_reference_canonicalize() {
|
277 | 293 | let sample_digest =
|
278 | 294 | "sha256:5db6d8b5f34d3cbdaa1e82ed0152a5ac980076d19317d4269db149cbde057bb2";
|
| 295 | + |
279 | 296 | let test_cases = [
|
280 | 297 | // When both a tag and digest are present, the digest should be used
|
281 | 298 | (
|
282 | 299 | format!("quay.io/example/someimage:latest@{}", sample_digest),
|
283 | 300 | format!("quay.io/example/someimage@{}", sample_digest),
|
| 301 | + "registry", |
| 302 | + ), |
| 303 | + ( |
| 304 | + format!("quay.io/example/someimage:latest@{}", sample_digest), |
| 305 | + format!("quay.io/example/someimage@{}", sample_digest), |
| 306 | + "containers-storage", |
| 307 | + ), |
| 308 | + ( |
| 309 | + format!("quay.io/example/someimage:latest@{}", sample_digest), |
| 310 | + format!("quay.io/example/someimage@{}", sample_digest), |
| 311 | + "oci", |
284 | 312 | ),
|
285 | 313 | // When only a digest is present, it should be used
|
286 | 314 | (
|
287 | 315 | format!("quay.io/example/someimage@{}", sample_digest),
|
288 | 316 | format!("quay.io/example/someimage@{}", sample_digest),
|
| 317 | + "registry", |
289 | 318 | ),
|
290 | 319 | // When only a tag is present, it should be preserved
|
291 | 320 | (
|
292 | 321 | "quay.io/example/someimage:latest".to_string(),
|
293 | 322 | "quay.io/example/someimage:latest".to_string(),
|
| 323 | + "registry", |
294 | 324 | ),
|
295 | 325 | // When no tag or digest is present, preserve the original image name
|
296 | 326 | (
|
297 | 327 | "quay.io/example/someimage".to_string(),
|
298 | 328 | "quay.io/example/someimage".to_string(),
|
| 329 | + "registry", |
299 | 330 | ),
|
300 | 331 | // When used with a local image (i.e. from containers-storage), the functionality should
|
301 | 332 | // be the same as previous cases
|
302 | 333 | (
|
303 | 334 | "localhost/someimage:latest".to_string(),
|
304 | 335 | "localhost/someimage:latest".to_string(),
|
| 336 | + "registry", |
305 | 337 | ),
|
306 | 338 | (
|
307 | 339 | format!("localhost/someimage:latest@{sample_digest}"),
|
308 | 340 | format!("localhost/someimage@{sample_digest}"),
|
| 341 | + "registry", |
| 342 | + ), |
| 343 | + // OCI Archive / Dir should be preserved, and canonicalize should be a no-op |
| 344 | + ( |
| 345 | + "/tmp/repo".to_string(), |
| 346 | + "/tmp/repo".to_string(), |
| 347 | + "oci-archive", |
| 348 | + ), |
| 349 | + ( |
| 350 | + "/tmp/image-dir".to_string(), |
| 351 | + "/tmp/image-dir".to_string(), |
| 352 | + "dir", |
309 | 353 | ),
|
310 | 354 | ];
|
311 | 355 |
|
312 |
| - for (initial, expected) in test_cases { |
| 356 | + for (initial, expected, transport) in test_cases { |
313 | 357 | let imgref = ImageReference {
|
314 | 358 | image: initial.to_string(),
|
315 |
| - transport: "registry".to_string(), |
| 359 | + transport: transport.to_string(), |
316 | 360 | signature: None,
|
317 | 361 | };
|
318 | 362 |
|
319 | 363 | let canonicalized = imgref.canonicalize();
|
320 | 364 | if let Err(e) = canonicalized {
|
321 |
| - panic!("Failed to canonicalize {initial}: {e}"); |
| 365 | + panic!("Failed to canonicalize {initial} with transport {transport}: {e}"); |
322 | 366 | }
|
323 | 367 | let canonicalized = canonicalized.unwrap();
|
324 |
| - assert_eq!(canonicalized.image, expected); |
325 |
| - assert_eq!(canonicalized.transport, "registry"); |
| 368 | + assert_eq!( |
| 369 | + canonicalized.image, expected, |
| 370 | + "Mismatch for transport {transport}" |
| 371 | + ); |
| 372 | + assert_eq!(canonicalized.transport, transport); |
326 | 373 | assert_eq!(canonicalized.signature, None);
|
327 | 374 | }
|
328 | 375 | }
|
|
0 commit comments