diff --git a/src/client.rs b/src/client.rs index d17ac327..1f484ce0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -867,11 +867,7 @@ impl Client { match digest { Some(digest) => { debug!("Selected manifest entry with digest: {}", digest); - let manifest_entry_reference = Reference::with_digest( - image.registry().to_string(), - image.repository().to_string(), - digest.clone(), - ); + let manifest_entry_reference = image.clone_with_digest(digest.clone()); self._pull_manifest(&manifest_entry_reference) .await .and_then(|(manifest, _digest)| match manifest { @@ -1094,7 +1090,7 @@ impl Client { offset: Option, ) -> Result { let layer = layer.as_layer_descriptor(); - let url = self.to_v2_blob_url(image.resolve_registry(), image.repository(), layer.digest); + let url = self.to_v2_blob_url(image, layer.digest); let mut request = RequestBuilderWrapper::from_client(self, |client| client.get(&url)) .apply_accept(MIME_TYPES_DISTRIBUTION_MANIFEST)? @@ -1433,11 +1429,10 @@ impl Client { ) -> Result { let lh = location_header.to_str()?; if lh.starts_with("/v2/") { + let registry = image.resolve_registry(); Ok(format!( - "{}://{}{}", - self.config.protocol.scheme_for(image.resolve_registry()), - image.resolve_registry(), - lh + "{scheme}://{registry}{lh}", + scheme = self.config.protocol.scheme_for(registry) )) } else { Ok(lh.to_string()) @@ -1446,57 +1441,52 @@ impl Client { /// Convert a Reference to a v2 manifest URL. fn to_v2_manifest_url(&self, reference: &Reference) -> String { - if let Some(digest) = reference.digest() { - format!( - "{}://{}/v2/{}/manifests/{}", - self.config - .protocol - .scheme_for(reference.resolve_registry()), - reference.resolve_registry(), - reference.repository(), - digest, - ) - } else { - format!( - "{}://{}/v2/{}/manifests/{}", - self.config - .protocol - .scheme_for(reference.resolve_registry()), - reference.resolve_registry(), - reference.repository(), + let registry = reference.resolve_registry(); + format!( + "{scheme}://{registry}/v2/{repository}/manifests/{reference}{ns}", + scheme = self.config.protocol.scheme_for(registry), + repository = reference.repository(), + reference = if let Some(digest) = reference.digest() { + digest + } else { reference.tag().unwrap_or("latest") - ) - } + }, + ns = reference + .namespace() + .map(|ns| format!("?ns={ns}")) + .unwrap_or_default(), + ) } /// Convert a Reference to a v2 blob (layer) URL. - fn to_v2_blob_url(&self, registry: &str, repository: &str, digest: &str) -> String { + fn to_v2_blob_url(&self, reference: &Reference, digest: &str) -> String { + let registry = reference.resolve_registry(); format!( - "{}://{}/v2/{}/blobs/{}", - self.config.protocol.scheme_for(registry), - registry, - repository, - digest, + "{scheme}://{registry}/v2/{repository}/blobs/{digest}{ns}", + scheme = self.config.protocol.scheme_for(registry), + repository = reference.repository(), + ns = reference + .namespace() + .map(|ns| format!("?ns={ns}")) + .unwrap_or_default(), ) } /// Convert a Reference to a v2 blob upload URL. fn to_v2_blob_upload_url(&self, reference: &Reference) -> String { - self.to_v2_blob_url( - reference.resolve_registry(), - reference.repository(), - "uploads/", - ) + self.to_v2_blob_url(reference, "uploads/") } fn to_list_tags_url(&self, reference: &Reference) -> String { + let registry = reference.resolve_registry(); format!( - "{}://{}/v2/{}/tags/list", - self.config - .protocol - .scheme_for(reference.resolve_registry()), - reference.resolve_registry(), - reference.repository(), + "{scheme}://{registry}/v2/{repository}/tags/list{ns}", + scheme = self.config.protocol.scheme_for(registry), + repository = reference.repository(), + ns = reference + .namespace() + .map(|ns| format!("?ns={ns}")) + .unwrap_or_default(), ) } } @@ -1941,6 +1931,7 @@ fn digest_header_value(headers: HeaderMap, body: Option<&[u8]>) -> Result, repository: String, tag: Option, digest: Option, @@ -85,6 +86,7 @@ impl Reference { pub fn with_tag(registry: String, repository: String, tag: String) -> Self { Self { registry, + mirror_registry: None, repository, tag: Some(tag), digest: None, @@ -95,45 +97,89 @@ impl Reference { pub fn with_digest(registry: String, repository: String, digest: String) -> Self { Self { registry, + mirror_registry: None, repository, tag: None, digest: Some(digest), } } + /// Clone the Reference for the same image with a new digest. + pub fn clone_with_digest(&self, digest: String) -> Self { + Self { + registry: self.registry.clone(), + mirror_registry: self.mirror_registry.clone(), + repository: self.repository.clone(), + tag: None, + digest: Some(digest), + } + } + + /// Set a pull mirror registry for this reference. + /// + /// The mirror registry will be used to resolve the image, the original registry + /// is available via the [`Reference::namespace`] function. + /// + /// The original registry will be sent with the `ns` query parameter to the mirror registry. + /// The `ns` query parameter is currently not part of the stable OCI Distribution Spec yet, + /// but is being discussed to be added and is already used by some other implementations + /// (for example containerd). So be aware that this feature might not work with all registries. + /// + /// Since this is not part of the stable OCI Distribution Spec yet, this feature is exempt from + /// semver backwards compatibility guarantees and might change in the future. + #[doc(hidden)] + pub fn set_mirror_registry(&mut self, registry: String) { + self.mirror_registry = Some(registry); + } + /// Resolve the registry address of a given `Reference`. /// /// Some registries, such as docker.io, uses a different address for the actual /// registry. This function implements such redirection. + /// + /// If a mirror registry is set, it will be used instead of the original registry. pub fn resolve_registry(&self) -> &str { - let registry = self.registry(); - match registry { - "docker.io" => "index.docker.io", - _ => registry, + match (self.registry(), self.mirror_registry.as_deref()) { + (_, Some(mirror_registry)) => mirror_registry, + ("docker.io", None) => "index.docker.io", + (registry, None) => registry, } } - /// registry returns the name of the registry. + /// Returns the name of the registry. pub fn registry(&self) -> &str { &self.registry } - /// repository returns the name of the repository. + /// Returns the name of the repository. pub fn repository(&self) -> &str { &self.repository } - /// tag returns the object's tag, if present. + /// Returns the object's tag, if present. pub fn tag(&self) -> Option<&str> { self.tag.as_deref() } - /// digest returns the object's digest, if present. + /// Returns the object's digest, if present. pub fn digest(&self) -> Option<&str> { self.digest.as_deref() } - /// full_name returns the full repository name and path. + /// Returns the original registry when pulled via a mirror. + /// + /// Since this is not part of the stable OCI Distribution Spec yet, this feature is exempt from + /// semver backwards compatibility guarantees and might change in the future. + #[doc(hidden)] + pub fn namespace(&self) -> Option<&str> { + if self.mirror_registry.is_some() { + Some(self.registry()) + } else { + None + } + } + + /// Returns the full repository name and path. fn full_name(&self) -> String { if self.registry() == "" { self.repository().to_string() @@ -142,7 +188,7 @@ impl Reference { } } - /// whole returns the whole reference. + /// Returns the whole reference. pub fn whole(&self) -> String { let mut s = self.full_name(); if let Some(t) = self.tag() { @@ -200,6 +246,7 @@ impl TryFrom for Reference { let (registry, repository) = split_domain(name); let reference = Reference { registry, + mirror_registry: None, repository, tag, digest, @@ -209,33 +256,25 @@ impl TryFrom for Reference { } // Digests much always be hex-encoded, ensuring that their hex portion will always be // size*2 - if reference.digest().is_some() { - let d = reference.digest().unwrap(); - // FIXME: we should actually separate the algorithm from the digest - // using regular expressions. This won't hold up if we support an - // algorithm more or less than 6 characters like sha1024. - if d.len() < 8 { - return Err(ParseError::DigestInvalidFormat); - } - let algo = &d[0..6]; - let digest = &d[7..]; - match algo { - "sha256" => { + if let Some(digest) = reference.digest() { + match digest.split_once(':') { + None => return Err(ParseError::DigestInvalidFormat), + Some(("sha256", digest)) => { if digest.len() != 64 { return Err(ParseError::DigestInvalidLength); } } - "sha384" => { + Some(("sha384", digest)) => { if digest.len() != 96 { return Err(ParseError::DigestInvalidLength); } } - "sha512" => { + Some(("sha512", digest)) => { if digest.len() != 128 { return Err(ParseError::DigestInvalidLength); } } - _ => return Err(ParseError::DigestUnsupported), + Some((_, _)) => return Err(ParseError::DigestUnsupported), } } Ok(reference) @@ -356,5 +395,45 @@ mod test { fn parse_bad_reference(input: &str, err: ParseError) { assert_eq!(Reference::try_from(input).unwrap_err(), err) } + + #[rstest( + input, + registry, + resolved_registry, + whole, + case( + "busybox", + "docker.io", + "index.docker.io", + "docker.io/library/busybox:latest" + ), + case("test.com/repo:tag", "test.com", "test.com", "test.com/repo:tag"), + case("test:5000/repo", "test:5000", "test:5000", "test:5000/repo:latest"), + case( + "sub-dom1.foo.com/bar/baz/quux", + "sub-dom1.foo.com", + "sub-dom1.foo.com", + "sub-dom1.foo.com/bar/baz/quux:latest" + ), + case( + "b.gcr.io/test.example.com/my-app:test.example.com", + "b.gcr.io", + "b.gcr.io", + "b.gcr.io/test.example.com/my-app:test.example.com" + ) + )] + fn test_mirror_registry(input: &str, registry: &str, resolved_registry: &str, whole: &str) { + let mut reference = Reference::try_from(input).expect("could not parse reference"); + assert_eq!(resolved_registry, reference.resolve_registry()); + assert_eq!(registry, reference.registry()); + assert_eq!(None, reference.namespace()); + assert_eq!(whole, reference.whole()); + + reference.set_mirror_registry("docker.mirror.io".to_owned()); + assert_eq!("docker.mirror.io", reference.resolve_registry()); + assert_eq!(registry, reference.registry()); + assert_eq!(Some(registry), reference.namespace()); + assert_eq!(whole, reference.whole()); + } } }