Skip to content

Commit

Permalink
Traits to abstract oci-dir and oci-archive formats (#117)
Browse files Browse the repository at this point in the history
Split from #108

- This PR introduces `ImageLayout` and `ImageLayoutBuilder`
- `ImageLayout` abstracts a storage which satisfies [OCI Image
layout](https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md),
i.e. `index.json`, `blobs/`, and `oci-layout` file
- In addition, `ImageLayout` assumes single layout contains single
manifest.
- Refactoring of `ocipkg::image::Builder` is done in this PR, and
refactoring of `ocipkg::image::Archive` will be next PR.
  • Loading branch information
termoshtt committed May 2, 2024
1 parent 3a60c46 commit 4734704
Show file tree
Hide file tree
Showing 10 changed files with 375 additions and 199 deletions.
8 changes: 2 additions & 6 deletions ocipkg-cli/src/bin/cargo-ocipkg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use colored::Colorize;
use ocipkg::ImageName;
use std::{
collections::hash_map::DefaultHasher,
fs,
hash::{Hash, Hasher},
path::{Path, PathBuf},
process::Command,
Expand Down Expand Up @@ -207,12 +206,9 @@ fn main() -> Result<()> {
"Creating".green().bold(),
dest.display()
);
let f = fs::File::create(dest)?;
let mut b = ocipkg::image::Builder::new(f);
b.set_name(&image_name);
b.set_annotations(annotations);
let mut b = ocipkg::image::Builder::new(dest, image_name.clone())?;
b.append_files(&targets)?;
let _output = b.into_inner()?;
let _artifact = b.build()?;
}
}

Expand Down
49 changes: 14 additions & 35 deletions ocipkg-cli/src/bin/ocipkg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ enum Opt {
/// Name of container, use UUID v4 hyphenated if not set.
#[arg(short = 't', long = "tag")]
tag: Option<String>,

/// Path to annotations file.
#[arg(default_value = "ocipkg.toml")]
annotations: PathBuf,
},

/// Compose files into an oci-archive tar file
Expand All @@ -42,10 +38,6 @@ enum Opt {
/// Name of container, use UUID v4 hyphenated if not set.
#[arg(short = 't', long = "tag")]
tag: Option<String>,

/// Path to annotations file.
#[arg(long = "annotations", default_value = "ocipkg.toml")]
annotations: PathBuf,
},

/// Load and expand container local cache
Expand Down Expand Up @@ -102,47 +94,34 @@ fn main() -> Result<()> {
input_directory,
output,
tag,
annotations,
} => {
let mut output = output;
output.set_extension("tar");
let f = fs::File::create(output)?;
let mut b = ocipkg::image::Builder::new(f);
if let Some(name) = tag {
b.set_name(&ocipkg::ImageName::parse(&name)?);
}
if annotations.is_file() {
let f = fs::read(annotations)?;
let input = String::from_utf8(f).expect("Non-UTF8 string in TOML");
b.set_annotations(
ocipkg::image::annotations::nested::Annotations::from_toml(&input)?.into(),
)
}
let image_name = if let Some(name) = tag {
ocipkg::ImageName::parse(&name)?
} else {
ocipkg::ImageName::default()
};
let mut b = ocipkg::image::Builder::new(output, image_name)?;
b.append_dir_all(&input_directory)?;
let _output = b.into_inner()?;
let _artifact = b.build()?;
}

Opt::Compose {
inputs,
output,
tag,
annotations,
} => {
let mut output = output;
output.set_extension("tar");
let f = fs::File::create(output)?;
let mut b = ocipkg::image::Builder::new(f);
if let Some(name) = tag {
b.set_name(&ocipkg::ImageName::parse(&name)?);
}
if annotations.is_file() {
let f = fs::read(annotations)?;
let input = String::from_utf8(f).expect("Non-UTF8 string in TOML");
b.set_annotations(
ocipkg::image::annotations::nested::Annotations::from_toml(&input)?.into(),
)
}
let image_name = if let Some(name) = tag {
ocipkg::ImageName::parse(&name)?
} else {
ocipkg::ImageName::default()
};
let mut b = ocipkg::image::Builder::new(output, image_name)?;
b.append_files(&inputs)?;
let _artifact = b.build()?;
}

Opt::Load { input } => {
Expand Down
1 change: 1 addition & 0 deletions ocipkg/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ directories = "5.0.1"
flate2 = "1.0.28"
lazy_static = "1.4.0"
log = "0.4.21"
maplit = "1.0.2"
oci-spec = "0.6.5"
regex = "1.10.4"
serde = "1.0.198"
Expand Down
4 changes: 4 additions & 0 deletions ocipkg/src/digest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ impl Digest {
}
}

pub fn from_descriptor(descriptor: &oci_spec::image::Descriptor) -> Result<Self> {
Self::new(descriptor.digest())
}

/// As a path used in oci-archive
pub fn as_path(&self) -> PathBuf {
PathBuf::from(format!("blobs/{}/{}", self.algorithm, self.encoded))
Expand Down
79 changes: 79 additions & 0 deletions ocipkg/src/image/artifact.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use crate::{image::ImageLayoutBuilder, ImageName};
use anyhow::Result;
use oci_spec::image::{
Descriptor, DescriptorBuilder, ImageManifest, ImageManifestBuilder, MediaType,
};
use std::collections::HashMap;

/// Create a new OCI Artifact over [ImageLayoutBuilder]
///
/// This creates a generic OCI Artifact, not the ocipkg artifact defined as `application/vnd.ocipkg.v1.artifact`.
/// It is the task of the [crate::image::Builder].
pub struct ArtifactBuilder<Base: ImageLayoutBuilder> {
name: ImageName,
manifest: ImageManifest,
layout: Base,
}

impl<Base: ImageLayoutBuilder> ArtifactBuilder<Base> {
/// Create a new OCI Artifact with its media type
pub fn new(mut layout: Base, artifact_type: MediaType, name: ImageName) -> Result<Self> {
let empty_config = layout.add_empty_json()?;
let manifest = ImageManifestBuilder::default()
.schema_version(2_u32)
.artifact_type(artifact_type)
.config(empty_config)
.layers(Vec::new())
.build()?;
Ok(Self {
layout,
manifest,
name,
})
}

/// Add `config` of the OCI Artifact
///
/// Image manifest of artifact can store any type of configuration blob.
pub fn add_config(
&mut self,
config_type: MediaType,
config_blob: &[u8],
annotations: HashMap<String, String>,
) -> Result<Descriptor> {
let (digest, size) = self.layout.add_blob(config_blob)?;
let config = DescriptorBuilder::default()
.media_type(config_type)
.annotations(annotations)
.digest(digest.to_string())
.size(size)
.build()?;
self.manifest.set_config(config.clone());
Ok(config)
}

/// Append a `layer` to the OCI Artifact
///
/// Image manifest of artifact can store any type of layer blob.
pub fn add_layer(
&mut self,
layer_type: MediaType,
layer_blob: &[u8],
annotations: HashMap<String, String>,
) -> Result<Descriptor> {
let (digest, size) = self.layout.add_blob(layer_blob)?;
let layer = DescriptorBuilder::default()
.media_type(layer_type)
.digest(digest.to_string())
.size(size)
.annotations(annotations)
.build()?;
self.manifest.layers_mut().push(layer.clone());
Ok(layer)
}

/// Build the OCI Artifact
pub fn build(self) -> Result<Base::ImageLayout> {
self.layout.build(self.manifest, self.name)
}
}
61 changes: 61 additions & 0 deletions ocipkg/src/image/layout.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use crate::{Digest, ImageName};
use anyhow::{Context, Result};
use oci_spec::image::{Descriptor, DescriptorBuilder, ImageIndex, ImageManifest, MediaType};

/// Handler of [OCI Image Layout] containing single manifest.
///
/// Though the [OCI Image Layout] allows containing multiple manifests in a single layout,
/// this trait assumes a single manifest in a single layout.
///
/// [OCI Image Layout]: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md
///
pub trait ImageLayout {
/// Get `index.json`
fn get_index(&mut self) -> Result<ImageIndex>;
/// Get blob content.
fn get_blob(&mut self, digest: &Digest) -> Result<Vec<u8>>;

/// Get manifest stored in the image layout.
///
/// Note that this trait assumes a single manifest in a single layout.
/// If `index.json` contains `org.opencontainers.image.ref.name` annotation, it is returned as [ImageName].
fn get_manifest(&mut self) -> Result<(Option<ImageName>, ImageManifest)> {
let index = self.get_index()?;
let desc = index.manifests().first().context("Missing manifest")?;
let name = if let Some(name) = desc
.annotations()
.as_ref()
.and_then(|annotations| annotations.get("org.opencontainers.image.ref.name"))
{
// Invalid image name raises an error, while missing name is just ignored.
Some(ImageName::parse(name)?)
} else {
None
};
let digest = Digest::from_descriptor(desc)?;
let manifest = serde_json::from_slice(self.get_blob(&digest)?.as_slice())?;
Ok((name, manifest))
}
}

/// Create new image layout.
///
/// Creating [ImageManifest] is out of scope of this trait.
pub trait ImageLayoutBuilder {
/// Handler of generated image.
type ImageLayout: ImageLayout;
/// Add a blob to the image layout.
fn add_blob(&mut self, data: &[u8]) -> Result<(Digest, i64)>;
/// Finish building image layout.
fn build(self, manifest: ImageManifest, name: ImageName) -> Result<Self::ImageLayout>;

/// A placeholder for `application/vnd.oci.empty.v1+json`
fn add_empty_json(&mut self) -> Result<Descriptor> {
let (digest, size) = self.add_blob(b"{}")?;
Ok(DescriptorBuilder::default()
.media_type(MediaType::EmptyJSON)
.size(size)
.digest(digest.to_string())
.build()?)
}
}
10 changes: 9 additions & 1 deletion ocipkg/src/image/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
//! Read and Write images based on [OCI image specification](https://github.com/opencontainers/image-spec)
//! Read and Write ocipkg artifacts defined as `application/vnd.ocipkg.v1.artifact`
//!
//! See the crate level documentation for more information.

pub mod annotations;

mod artifact;
mod config;
mod layout;
mod oci_archive;
mod read;
mod write;

pub use artifact::*;
pub use config::*;
pub use layout::*;
pub use oci_archive::*;
pub use read::*;
pub use write::*;
Loading

0 comments on commit 4734704

Please sign in to comment.