diff --git a/crates/oci/src/fetch.rs b/crates/oci/src/fetch.rs index fc4ce4a..1795f12 100644 --- a/crates/oci/src/fetch.rs +++ b/crates/oci/src/fetch.rs @@ -217,6 +217,13 @@ impl OciImageFetcher { continue; } } + + if let Some(ref digest) = image.digest { + if digest != manifest.digest() { + continue; + } + } + found = Some(manifest); break; } @@ -240,7 +247,7 @@ impl OciImageFetcher { let mut client = OciRegistryClient::new(image.registry_url()?, self.platform.clone())?; let (manifest, descriptor, digest) = client - .get_manifest_with_digest(&image.name, &image.reference) + .get_manifest_with_digest(&image.name, image.reference.as_ref(), image.digest.as_ref()) .await?; let descriptor = descriptor.unwrap_or_else(|| { DescriptorBuilder::default() diff --git a/crates/oci/src/name.rs b/crates/oci/src/name.rs index ab92da1..5babe71 100644 --- a/crates/oci/src/name.rs +++ b/crates/oci/src/name.rs @@ -2,33 +2,39 @@ use anyhow::Result; use std::fmt; use url::Url; -const DOCKER_HUB_MIRROR: &str = "mirror.gcr.io"; -const DEFAULT_IMAGE_TAG: &str = "latest"; - #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ImageName { pub hostname: String, pub port: Option, pub name: String, - pub reference: String, + pub reference: Option, + pub digest: Option, } impl fmt::Display for ImageName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if DOCKER_HUB_MIRROR == self.hostname && self.port.is_none() { + let mut suffix = String::new(); + + if let Some(ref reference) = self.reference { + suffix.push(':'); + suffix.push_str(reference); + } + + if let Some(ref digest) = self.digest { + suffix.push('@'); + suffix.push_str(digest); + } + + if ImageName::DOCKER_HUB_MIRROR == self.hostname && self.port.is_none() { if self.name.starts_with("library/") { - write!(f, "{}:{}", &self.name[8..], self.reference) + write!(f, "{}{}", &self.name[8..], suffix) } else { - write!(f, "{}:{}", self.name, self.reference) + write!(f, "{}{}", self.name, suffix) } } else if let Some(port) = self.port { - write!( - f, - "{}:{}/{}:{}", - self.hostname, port, self.name, self.reference - ) + write!(f, "{}:{}/{}{}", self.hostname, port, self.name, suffix) } else { - write!(f, "{}/{}:{}", self.hostname, self.name, self.reference) + write!(f, "{}/{}{}", self.hostname, self.name, suffix) } } } @@ -41,13 +47,21 @@ impl Default for ImageName { } impl ImageName { + pub const DOCKER_HUB_MIRROR: &'static str = "registry.docker.io"; + pub const DEFAULT_IMAGE_TAG: &'static str = "latest"; + pub fn parse(name: &str) -> Result { let full_name = name.to_string(); let name = full_name.clone(); let (mut hostname, mut name) = name .split_once('/') .map(|x| (x.0.to_string(), x.1.to_string())) - .unwrap_or_else(|| (DOCKER_HUB_MIRROR.to_string(), format!("library/{}", name))); + .unwrap_or_else(|| { + ( + ImageName::DOCKER_HUB_MIRROR.to_string(), + format!("library/{}", name), + ) + }); // heuristic to find any docker hub image formats // that may be in the hostname format. for example: @@ -55,7 +69,7 @@ impl ImageName { // and neither will abc/hello/xyz:latest if !hostname.contains('.') && full_name.chars().filter(|x| *x == '/').count() == 1 { name = format!("{}/{}", hostname, name); - hostname = DOCKER_HUB_MIRROR.to_string(); + hostname = ImageName::DOCKER_HUB_MIRROR.to_string(); } let (hostname, port) = if let Some((hostname, port)) = hostname @@ -66,15 +80,54 @@ impl ImageName { } else { (hostname, None) }; - let (name, reference) = name - .split_once(':') - .map(|x| (x.0.to_string(), x.1.to_string())) - .unwrap_or((name.to_string(), DEFAULT_IMAGE_TAG.to_string())); + + let name_has_digest = if name.contains('@') { + let digest_start = name.chars().position(|c| c == '@'); + let ref_start = name.chars().position(|c| c == ':'); + if let (Some(digest_start), Some(ref_start)) = (digest_start, ref_start) { + digest_start < ref_start + } else { + true + } + } else { + false + }; + + let (name, digest) = if name_has_digest { + name.split_once('@') + .map(|(name, digest)| (name.to_string(), Some(digest.to_string()))) + .unwrap_or_else(|| (name, None)) + } else { + (name, None) + }; + + let (name, reference) = if name.contains(':') { + name.split_once(':') + .map(|(name, reference)| (name.to_string(), Some(reference.to_string()))) + .unwrap_or((name, None)) + } else { + (name, None) + }; + + let (reference, digest) = if let Some(reference) = reference { + if let Some(digest) = digest { + (Some(reference), Some(digest)) + } else { + reference + .split_once('@') + .map(|(reff, digest)| (Some(reff.to_string()), Some(digest.to_string()))) + .unwrap_or_else(|| (Some(reference), None)) + } + } else { + (None, digest) + }; + Ok(ImageName { hostname, port, name, reference, + digest, }) } diff --git a/crates/oci/src/packer/cache.rs b/crates/oci/src/packer/cache.rs index 32743c1..00b020b 100644 --- a/crates/oci/src/packer/cache.rs +++ b/crates/oci/src/packer/cache.rs @@ -159,7 +159,9 @@ impl OciPackerCache { let image_name = packed.name.to_string(); annotations.insert(ANNOTATION_IMAGE_NAME.to_string(), image_name); let image_ref = packed.name.reference.clone(); - annotations.insert(ANNOTATION_REF_NAME.to_string(), image_ref); + if let Some(image_ref) = image_ref { + annotations.insert(ANNOTATION_REF_NAME.to_string(), image_ref); + } descriptor.set_annotations(Some(annotations)); manifests.push(descriptor.clone()); index.set_manifests(manifests); diff --git a/crates/oci/src/packer/service.rs b/crates/oci/src/packer/service.rs index 9fff54b..8e563a1 100644 --- a/crates/oci/src/packer/service.rs +++ b/crates/oci/src/packer/service.rs @@ -61,6 +61,10 @@ impl OciPackerService { digest: &str, format: OciPackedFormat, ) -> Result> { + if digest.contains('/') || digest.contains('\\') || digest.contains("..") { + return Ok(None); + } + self.cache .recall(ImageName::parse("cached:latest")?, digest, format) .await diff --git a/crates/oci/src/registry.rs b/crates/oci/src/registry.rs index 1874ae8..4e4a857 100644 --- a/crates/oci/src/registry.rs +++ b/crates/oci/src/registry.rs @@ -7,7 +7,7 @@ use reqwest::{Client, RequestBuilder, Response, StatusCode}; use tokio::{fs::File, io::AsyncWriteExt}; use url::Url; -use crate::{progress::OciBoundProgress, schema::OciSchema}; +use crate::{name::ImageName, progress::OciBoundProgress, schema::OciSchema}; #[derive(Clone, Debug)] pub struct OciPlatform { @@ -176,7 +176,7 @@ impl OciRegistryClient { let url = self.url.join(&format!( "/v2/{}/manifests/{}", name.as_ref(), - reference.as_ref() + reference.as_ref(), ))?; let accept = format!( "{}, {}, {}, {}", @@ -202,13 +202,20 @@ impl OciRegistryClient { pub async fn get_manifest_with_digest, R: AsRef>( &mut self, name: N, - reference: R, + reference: Option, + digest: Option, ) -> Result<(OciSchema, Option, String)> { - let url = self.url.join(&format!( - "/v2/{}/manifests/{}", - name.as_ref(), - reference.as_ref() - ))?; + let what = digest + .as_ref() + .map(|x| x.as_ref().to_string()) + .unwrap_or_else(|| { + reference + .map(|x| x.as_ref().to_string()) + .unwrap_or_else(|| ImageName::DEFAULT_IMAGE_TAG.to_string()) + }); + let url = self + .url + .join(&format!("/v2/{}/manifests/{}", name.as_ref(), what,))?; let accept = format!( "{}, {}, {}, {}", MediaType::ImageManifest.to_docker_v2s2()?, @@ -239,9 +246,10 @@ impl OciRegistryClient { let digest = response .headers() .get("Docker-Content-Digest") - .ok_or_else(|| anyhow!("fetching manifest did not yield a content digest"))? - .to_str()? - .to_string(); + .and_then(|x| x.to_str().ok()) + .map(|x| x.to_string()) + .or_else(|| digest.map(|x: N| x.as_ref().to_string())) + .ok_or_else(|| anyhow!("fetching manifest did not yield a content digest"))?; let bytes = response.bytes().await?; let manifest = serde_json::from_slice(&bytes)?; Ok((OciSchema::new(bytes.to_vec(), manifest), None, digest))