-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
225 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
use itertools::Itertools; | ||
|
||
/// Implements `priority_find` for collections. | ||
pub trait PriorityFind<T> { | ||
/// Searches through a list of items using the provided prioritization function. | ||
/// Priorities are such that a lower number is higher priority, meaning that `0` is the highest possible priority. | ||
/// | ||
/// As the search is performed: | ||
/// - If an item with the lowest priority is found, it is immediately returned and the rest of the search is aborted. | ||
/// - Otherwise, the highest priority item found is retained until the end of the search, at which point it is returned. | ||
fn priority_find<F: Fn(&T) -> usize>(self, prioritize: F) -> Option<T>; | ||
} | ||
|
||
impl<T, I> PriorityFind<T> for I | ||
where | ||
I: Iterator<Item = T>, | ||
{ | ||
fn priority_find<F: Fn(&T) -> usize>(self, prioritize: F) -> Option<T> { | ||
priority_find(self, prioritize) | ||
} | ||
} | ||
|
||
/// Searches through a list of items using a priority function returning a non-negative number. | ||
/// Priorities are such that a lower number is higher priority, meaning that `0` is the highest possible priority. | ||
/// | ||
/// As the search is performed: | ||
/// - If an item with the lowest priority is found, it is immediately returned and the rest of the search is aborted. | ||
/// - Otherwise, the highest priority item found is retained until the end of the search, at which point it is returned. | ||
fn priority_find<T, F: Fn(&T) -> usize>( | ||
items: impl IntoIterator<Item = T>, | ||
prioritize: F, | ||
) -> Option<T> { | ||
items | ||
.into_iter() | ||
// Mapping here allows the function to use `take_while_inclusive` to bound the search below | ||
// instead of using more complex logic in `fold`. | ||
.map(|item| (prioritize(&item), item)) | ||
// This ensures that the fold stops after finding the first priority 0 item, which constitutes an early termination condition. | ||
// Any item that isn't at priority 0 doesn't allow the function to early return: it might find a higher priority item later. | ||
.take_while_inclusive(|(priority, _)| *priority > 0) | ||
// The job of fold is now simple: just always select the item with higher priority. | ||
.fold(None, |result, (incoming, item)| { | ||
match result { | ||
// No result yet, so incoming item is automatically highest priority. | ||
None => Some((incoming, item)), | ||
|
||
// Remember that "lower number" means "higher priority". | ||
// If the new item isn't higher priority, keep the current pick: | ||
// this ensures the first item encountered at a given priority is chosen. | ||
Some((current, _)) => { | ||
if current > incoming { | ||
Some((incoming, item)) | ||
} else { | ||
result | ||
} | ||
} | ||
} | ||
}) | ||
.map(|(_, item)| item) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,124 @@ | ||
//! Interacts with remote OCI registries. | ||
use color_eyre::eyre::Result; | ||
use std::str::FromStr; | ||
|
||
use crate::Reference; | ||
use color_eyre::eyre::{Context, Result}; | ||
use oci_client::{ | ||
client::ClientConfig, manifest::ImageIndexEntry, secrets::RegistryAuth, Client, | ||
Reference as OciReference, | ||
}; | ||
|
||
/// Enumerate layers for a reference in the remote registry. | ||
pub async fn layers(reference: &Reference) -> Result<Vec<String>> { | ||
Ok(vec![]) | ||
use crate::{ext::PriorityFind, LayerReference, Platform, Reference, Version}; | ||
|
||
/// Enumerate layers for a container reference in the remote registry. | ||
/// Layers are returned in order from the base image to the application. | ||
#[tracing::instrument] | ||
pub async fn layers( | ||
platform: Option<&Platform>, | ||
reference: &Reference, | ||
) -> Result<Vec<LayerReference>> { | ||
let client = client(platform.cloned()); | ||
let auth = RegistryAuth::Anonymous; | ||
|
||
let oci_ref = OciReference::from(reference); | ||
let (manifest, _) = client | ||
.pull_image_manifest(&oci_ref, &auth) | ||
.await | ||
.context("pull image manifest: {oci_ref}")?; | ||
|
||
manifest | ||
.layers | ||
.into_iter() | ||
.map(|layer| LayerReference::from_str(&layer.digest)) | ||
.collect() | ||
} | ||
|
||
impl From<&Reference> for OciReference { | ||
fn from(reference: &Reference) -> Self { | ||
match &reference.version { | ||
Version::Tag(tag) => Self::with_tag( | ||
reference.host.clone(), | ||
reference.repository.clone(), | ||
tag.clone(), | ||
), | ||
Version::Digest(digest) => Self::with_digest( | ||
reference.host.clone(), | ||
reference.repository.clone(), | ||
digest.to_string(), | ||
), | ||
} | ||
} | ||
} | ||
|
||
fn client(platform: Option<Platform>) -> Client { | ||
let mut config = ClientConfig::default(); | ||
config.platform_resolver = match platform { | ||
Some(platform) => Some(Box::new(target_platform_resolver(platform))), | ||
None => Some(Box::new(current_platform_resolver)), | ||
}; | ||
Client::new(config) | ||
} | ||
|
||
fn target_platform_resolver(target: Platform) -> impl Fn(&[ImageIndexEntry]) -> Option<String> { | ||
move |entries: &[ImageIndexEntry]| { | ||
entries | ||
.iter() | ||
.find(|entry| { | ||
entry.platform.as_ref().map_or(false, |platform| { | ||
platform.os == target.os && platform.architecture == target.architecture | ||
}) | ||
}) | ||
.map(|entry| entry.digest.clone()) | ||
} | ||
} | ||
|
||
fn current_platform_resolver(entries: &[ImageIndexEntry]) -> Option<String> { | ||
let current_os = go_os(); | ||
let current_arch = go_arch(); | ||
let linux = Platform::LINUX; | ||
let amd64 = Platform::AMD64; | ||
entries | ||
.iter() | ||
.priority_find(|entry| match entry.platform.as_ref() { | ||
None => 0, | ||
Some(p) if p.os == current_os && p.architecture == current_arch => 1, | ||
Some(p) if p.os == linux && p.architecture == current_arch => 2, | ||
Some(p) if p.os == linux && p.architecture == amd64 => 3, | ||
_ => 4, | ||
}) | ||
.map(|entry| entry.digest.clone()) | ||
} | ||
|
||
/// Returns the current OS as a string that matches a `GOOS` constant. | ||
/// This is required because the OCI spec requires the OS to be a valid GOOS value. | ||
// If you get a compile error here, you need to add a new `cfg` branch for your platform. | ||
// Valid GOOS values may be gathered from here: https://go.dev/doc/install/source#environment | ||
const fn go_os() -> &'static str { | ||
#[cfg(target_os = "linux")] | ||
{ | ||
"linux" | ||
} | ||
#[cfg(target_os = "macos")] | ||
{ | ||
"darwin" | ||
} | ||
#[cfg(target_os = "windows")] | ||
{ | ||
"windows" | ||
} | ||
} | ||
|
||
/// Returns the current architecture as a string that matches a `GOARCH` constant. | ||
/// This is required because the OCI spec requires the architecture to be a valid GOARCH value. | ||
// If you get a compile error here, you need to add a new `cfg` branch for your platform. | ||
// Valid GOARCH values may be gathered from here: https://go.dev/doc/install/source#environment | ||
const fn go_arch() -> &'static str { | ||
#[cfg(target_arch = "x86_64")] | ||
{ | ||
"amd64" | ||
} | ||
#[cfg(target_arch = "aarch64")] | ||
{ | ||
"arm64" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
mod platform; | ||
mod reference; | ||
mod registry; |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
use circe::{Platform, Reference}; | ||
use color_eyre::Result; | ||
use simple_test_case::test_case; | ||
|
||
#[test_case("docker.io/library/alpine:latest", None; "docker.io/library/alpine:latest")] | ||
#[test_case("docker.io/library/ubuntu:latest", None; "docker.io/library/ubuntu:latest")] | ||
#[tokio::test] | ||
async fn single_platform_layers(image: &str, platform: Option<Platform>) -> Result<()> { | ||
let reference = image.parse::<Reference>()?; | ||
let layers = circe::registry::layers(platform.as_ref(), &reference).await?; | ||
|
||
// Verify we got some layers back | ||
assert!(!layers.is_empty(), "image should have at least one layer"); | ||
Ok(()) | ||
} | ||
|
||
#[test_case("docker.io/library/golang:latest", Platform::linux_amd64(); "docker.io/library/golang:latest.linux_amd64")] | ||
#[test_case("docker.io/library/golang:latest", Platform::linux_arm64(); "docker.io/library/golang:latest.linux_arm64")] | ||
#[tokio::test] | ||
async fn multi_platform_layers(image: &str, platform: Platform) -> Result<()> { | ||
let reference = image.parse::<Reference>()?; | ||
let layers = circe::registry::layers(Some(&platform), &reference).await?; | ||
|
||
// Verify we got some layers back | ||
assert!(!layers.is_empty(), "image should have at least one layer"); | ||
Ok(()) | ||
} |