Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref(client): Debug, Eq, Hash, PartialEq for ImageLayer; convert annotations to BTreeMap #121

Merged
merged 2 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ clap = { version = "4.0", features = ["derive"] }
rstest = "0.18.1"
docker_credential = "1.0"
hmac = "0.12"
itertools = "0.12.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tempfile = "3.3"
testcontainers = "0.15"
Expand Down
6 changes: 3 additions & 3 deletions examples/wasm/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use oci_distribution::{annotations, secrets::RegistryAuth, Client, Reference};

use docker_credential::{CredentialRetrievalError, DockerCredential};
use std::collections::HashMap;
use std::collections::BTreeMap;
use tracing::{debug, warn};
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};
Expand Down Expand Up @@ -84,14 +84,14 @@ pub async fn main() {
let auth = build_auth(&reference, &cli);

let annotations = if annotations.is_empty() {
let mut values: HashMap<String, String> = HashMap::new();
let mut values: BTreeMap<String, String> = BTreeMap::new();
values.insert(
annotations::ORG_OPENCONTAINERS_IMAGE_TITLE.to_string(),
module.clone(),
);
Some(values)
} else {
let mut values: HashMap<String, String> = HashMap::new();
let mut values: BTreeMap<String, String> = BTreeMap::new();
for annotation in annotations {
let tmp: Vec<_> = annotation.splitn(2, '=').collect();
if tmp.len() == 2 {
Expand Down
4 changes: 2 additions & 2 deletions examples/wasm/push.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ use oci_distribution::{
secrets::RegistryAuth,
Client, Reference,
};
use std::collections::HashMap;
use std::collections::BTreeMap;
use tracing::info;

pub(crate) async fn push_wasm(
client: &mut Client,
auth: &RegistryAuth,
reference: &Reference,
module: &str,
annotations: Option<HashMap<String, String>>,
annotations: Option<BTreeMap<String, String>>,
) {
info!(?reference, ?module, "pushing wasm module");

Expand Down
106 changes: 96 additions & 10 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ use reqwest::{RequestBuilder, Url};
use serde::Deserialize;
use serde::Serialize;
use sha2::Digest;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::convert::TryFrom;
use std::hash::Hash;
use std::sync::Arc;
use tokio::io::{AsyncWrite, AsyncWriteExt};
use tokio::sync::RwLock;
Expand Down Expand Up @@ -86,23 +87,23 @@ pub struct TagResponse {
}

/// The data and media type for an image layer
#[derive(Clone)]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ImageLayer {
/// The data of this layer
pub data: Vec<u8>,
/// The media type of this layer
pub media_type: String,
/// This OPTIONAL property contains arbitrary metadata for this descriptor.
/// This OPTIONAL property MUST use the [annotation rules](https://github.com/opencontainers/image-spec/blob/main/annotations.md#rules)
pub annotations: Option<HashMap<String, String>>,
pub annotations: Option<BTreeMap<String, String>>,
}

impl ImageLayer {
/// Constructs a new ImageLayer struct with provided data and media type
pub fn new(
data: Vec<u8>,
media_type: String,
annotations: Option<HashMap<String, String>>,
annotations: Option<BTreeMap<String, String>>,
) -> Self {
ImageLayer {
data,
Expand All @@ -113,12 +114,12 @@ impl ImageLayer {

/// Constructs a new ImageLayer struct with provided data and
/// media type application/vnd.oci.image.layer.v1.tar
pub fn oci_v1(data: Vec<u8>, annotations: Option<HashMap<String, String>>) -> Self {
pub fn oci_v1(data: Vec<u8>, annotations: Option<BTreeMap<String, String>>) -> Self {
Self::new(data, IMAGE_LAYER_MEDIA_TYPE.to_string(), annotations)
}
/// Constructs a new ImageLayer struct with provided data and
/// media type application/vnd.oci.image.layer.v1.tar+gzip
pub fn oci_v1_gzip(data: Vec<u8>, annotations: Option<HashMap<String, String>>) -> Self {
pub fn oci_v1_gzip(data: Vec<u8>, annotations: Option<BTreeMap<String, String>>) -> Self {
Self::new(data, IMAGE_LAYER_GZIP_MEDIA_TYPE.to_string(), annotations)
}

Expand All @@ -137,15 +138,15 @@ pub struct Config {
pub media_type: String,
/// This OPTIONAL property contains arbitrary metadata for this descriptor.
/// This OPTIONAL property MUST use the [annotation rules](https://github.com/opencontainers/image-spec/blob/main/annotations.md#rules)
pub annotations: Option<HashMap<String, String>>,
pub annotations: Option<BTreeMap<String, String>>,
}

impl Config {
/// Constructs a new Config struct with provided data and media type
pub fn new(
data: Vec<u8>,
media_type: String,
annotations: Option<HashMap<String, String>>,
annotations: Option<BTreeMap<String, String>>,
) -> Self {
Config {
data,
Expand All @@ -156,15 +157,15 @@ impl Config {

/// Constructs a new Config struct with provided data and
/// media type application/vnd.oci.image.config.v1+json
pub fn oci_v1(data: Vec<u8>, annotations: Option<HashMap<String, String>>) -> Self {
pub fn oci_v1(data: Vec<u8>, annotations: Option<BTreeMap<String, String>>) -> Self {
Self::new(data, IMAGE_CONFIG_MEDIA_TYPE.to_string(), annotations)
}

/// Construct a new Config struct with provided [`ConfigFile`] and
/// media type `application/vnd.oci.image.config.v1+json`
pub fn oci_v1_from_config_file(
config_file: ConfigFile,
annotations: Option<HashMap<String, String>>,
annotations: Option<BTreeMap<String, String>>,
) -> Result<Self> {
let data = serde_json::to_vec(&config_file)?;
Ok(Self::new(
Expand Down Expand Up @@ -2929,4 +2930,89 @@ mod test {
.await
.expect("Failed to pull manifest");
}

#[tokio::test]
async fn test_hashable_image_layer() {
use itertools::Itertools;

// First two should be identical; others differ
let image_layers = Vec::from([
ImageLayer {
data: Vec::from([0, 1, 2, 3]),
media_type: "media_type".to_owned(),
annotations: Some(BTreeMap::from([
("0".to_owned(), "1".to_owned()),
("2".to_owned(), "3".to_owned()),
])),
},
ImageLayer {
data: Vec::from([0, 1, 2, 3]),
media_type: "media_type".to_owned(),
annotations: Some(BTreeMap::from([
("2".to_owned(), "3".to_owned()),
("0".to_owned(), "1".to_owned()),
])),
},
ImageLayer {
data: Vec::from([0, 1, 2, 3]),
media_type: "different_media_type".to_owned(),
annotations: Some(BTreeMap::from([
("0".to_owned(), "1".to_owned()),
("2".to_owned(), "3".to_owned()),
])),
},
ImageLayer {
data: Vec::from([0, 1, 2]),
media_type: "media_type".to_owned(),
annotations: Some(BTreeMap::from([
("0".to_owned(), "1".to_owned()),
("2".to_owned(), "3".to_owned()),
])),
},
ImageLayer {
data: Vec::from([0, 1, 2, 3]),
media_type: "media_type".to_owned(),
annotations: Some(BTreeMap::from([
("1".to_owned(), "0".to_owned()),
("2".to_owned(), "3".to_owned()),
])),
},
]);

assert_eq!(
&image_layers[0], &image_layers[1],
"image_layers[0] should equal image_layers[1]"
);
assert_ne!(
&image_layers[0], &image_layers[2],
"image_layers[0] should not equal image_layers[2]"
);
assert_ne!(
&image_layers[0], &image_layers[3],
"image_layers[0] should not equal image_layers[3]"
);
assert_ne!(
&image_layers[0], &image_layers[4],
"image_layers[0] should not equal image_layers[4]"
);
assert_ne!(
&image_layers[2], &image_layers[3],
"image_layers[2] should not equal image_layers[3]"
);
assert_ne!(
&image_layers[2], &image_layers[4],
"image_layers[2] should not equal image_layers[4]"
);
assert_ne!(
&image_layers[3], &image_layers[4],
"image_layers[3] should not equal image_layers[4]"
);

let deduped: Vec<ImageLayer> = image_layers.clone().into_iter().unique().collect();
assert_eq!(
image_layers.len() - 1,
deduped.len(),
"after deduplication, there should be one less image layer"
);
}
}
12 changes: 6 additions & 6 deletions src/manifest.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! OCI Manifest
use std::collections::HashMap;
use std::collections::BTreeMap;

use crate::{
client::{Config, ImageLayer},
Expand Down Expand Up @@ -113,7 +113,7 @@ pub struct OciImageManifest {
/// MUST either be absent or be an empty map."
/// TO accomodate either, this is optional.
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<HashMap<String, String>>,
pub annotations: Option<BTreeMap<String, String>>,
}

impl Default for OciImageManifest {
Expand All @@ -137,7 +137,7 @@ impl OciImageManifest {
pub fn build(
layers: &[ImageLayer],
config: &Config,
annotations: Option<HashMap<String, String>>,
annotations: Option<BTreeMap<String, String>>,
) -> Self {
let mut manifest = OciImageManifest::default();

Expand Down Expand Up @@ -276,7 +276,7 @@ pub struct OciDescriptor {
/// This OPTIONAL property MUST use the annotation rules.
/// <https://github.com/opencontainers/image-spec/blob/main/annotations.md#rules>
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<HashMap<String, String>>,
pub annotations: Option<BTreeMap<String, String>>,
}

impl std::fmt::Display for OciDescriptor {
Expand Down Expand Up @@ -334,7 +334,7 @@ pub struct OciImageIndex {
/// MUST either be absent or be an empty map."
/// TO accomodate either, this is optional.
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<HashMap<String, String>>,
pub annotations: Option<BTreeMap<String, String>>,
}

/// The manifest entry of an `ImageIndex`.
Expand Down Expand Up @@ -375,7 +375,7 @@ pub struct ImageIndexEntry {
/// This OPTIONAL property contains arbitrary metadata for the image index.
/// This OPTIONAL property MUST use the [annotation rules](https://github.com/opencontainers/image-spec/blob/main/annotations.md#rules).
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<HashMap<String, String>>,
pub annotations: Option<BTreeMap<String, String>>,
}

impl std::fmt::Display for ImageIndexEntry {
Expand Down
Loading