use crate::errors::Error;
use crate::Result;
use oci_client::client::{current_platform_resolver, ClientConfig};
use oci_client::manifest::{ImageIndexEntry, OciImageManifest};
use oci_client::{Client, Reference};
use oci_spec::image::{Arch, Os};
use std::collections::BTreeSet;
type PlatformResolver = Box<dyn Fn(&[ImageIndexEntry]) -> Option<String> + Send + Sync>;
pub(crate) fn build_blob_url(registry: &str, repository: &str, digest: &str) -> String {
format!("https://{registry}/v2/{repository}/blobs/{digest}")
}
pub(crate) fn build_manifest_url(registry: &str, repository: &str, reference: &str) -> String {
format!("https://{registry}/v2/{repository}/manifests/{reference}")
}
pub(crate) fn blob_digests(manifest: &OciImageManifest) -> BTreeSet<&str> {
std::iter::once(manifest.config.digest.as_str())
.chain(manifest.layers.iter().map(|layer| layer.digest.as_str()))
.collect()
}
pub(crate) fn resolve_registry(reference: &Reference) -> &str {
match reference.registry() {
"docker.io" => "registry-1.docker.io",
registry => registry,
}
}
fn platform_resolver(platform: &str) -> Result<PlatformResolver> {
let (os, arch) = platform
.split_once('/')
.map(|(os, arch)| (Os::from(os), Arch::from(arch)))
.ok_or_else(|| {
Error::InvalidArgument(format!(
"invalid platform format '{platform}', expected 'os/arch' (e.g., 'linux/amd64')"
))
})?;
Ok(Box::new(move |manifests: &[ImageIndexEntry]| {
manifests
.iter()
.find(|entry| {
entry
.platform
.as_ref()
.is_some_and(|platform| platform.os == os && platform.architecture == arch)
})
.map(|entry| entry.digest.clone())
}))
}
pub(crate) fn oci_client(platform: Option<String>) -> Result<Client> {
let oci_config = ClientConfig {
platform_resolver: match platform {
Some(platform) => Some(platform_resolver(&platform)?),
None => Some(Box::new(current_platform_resolver)),
},
..ClientConfig::default()
};
Ok(Client::new(oci_config))
}
#[cfg(test)]
mod tests {
use super::*;
use oci_client::manifest::{OciDescriptor, IMAGE_MANIFEST_MEDIA_TYPE};
#[test]
fn build_blob_url_uses_https_by_default() {
let url = build_blob_url("registry.example.com", "library/nginx", "sha256:abcdef");
assert_eq!(
url,
"https://registry.example.com/v2/library/nginx/blobs/sha256:abcdef"
);
}
#[test]
fn build_manifest_url_uses_https_by_default() {
let url = build_manifest_url("registry.example.com", "library/nginx", "latest");
assert_eq!(
url,
"https://registry.example.com/v2/library/nginx/manifests/latest"
);
}
#[test]
fn blob_digests_dedupes_the_config_and_layers() {
let descriptor = |digest: &str| OciDescriptor {
digest: digest.to_string(),
..Default::default()
};
let manifest = OciImageManifest {
config: descriptor("sha256:cccc"),
layers: vec![
descriptor("sha256:bbbb"),
descriptor("sha256:aaaa"),
descriptor("sha256:bbbb"),
descriptor("sha256:cccc"),
],
..Default::default()
};
assert_eq!(
blob_digests(&manifest).into_iter().collect::<Vec<_>>(),
vec!["sha256:aaaa", "sha256:bbbb", "sha256:cccc"]
);
}
#[test]
fn resolve_registry_maps_docker_hub() {
let test_cases = vec![
("nginx", "registry-1.docker.io"),
("example.com/foo/bar:1.0", "example.com"),
];
for (image, expected) in test_cases {
let reference: Reference = image.parse().unwrap();
assert_eq!(resolve_registry(&reference), expected);
}
}
#[test]
fn platform_resolver_matches_manifests() {
let manifests = vec![
image_index_entry("sha256:amd64", Some((Os::Linux, Arch::Amd64))),
image_index_entry("sha256:arm64", Some((Os::Linux, Arch::ARM64))),
image_index_entry("sha256:no-platform", None),
];
let test_cases = vec![
("linux/amd64", Ok(Some("sha256:amd64"))),
("linux/arm64", Ok(Some("sha256:arm64"))),
("windows/amd64", Ok(None)),
("linux/riscv64", Ok(None)),
("linux-amd64", Err("invalid platform format")),
("", Err("invalid platform format")),
];
for (platform, expected) in test_cases {
match expected {
Ok(expected_digest) => {
let resolver = platform_resolver(platform).unwrap();
assert_eq!(
resolver(&manifests),
expected_digest.map(|digest| digest.to_string()),
"platform: {platform}"
);
}
Err(expected_message) => {
assert!(
matches!(platform_resolver(platform), Err(Error::InvalidArgument(message)) if message.contains(expected_message)),
"platform: {platform}"
);
}
}
}
}
fn image_index_entry(digest: &str, platform: Option<(Os, Arch)>) -> ImageIndexEntry {
ImageIndexEntry {
media_type: IMAGE_MANIFEST_MEDIA_TYPE.to_string(),
digest: digest.to_string(),
size: 0,
platform: platform.map(|(os, architecture)| oci_client::manifest::Platform {
architecture,
os,
os_version: None,
os_features: None,
variant: None,
features: None,
}),
annotations: None,
artifact_type: None,
}
}
}