Skip to content
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
8 changes: 8 additions & 0 deletions peeko/src/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ use std::fs;
use std::io::Result;
use std::path::{Path, PathBuf};

/// Returns a list of image name/tag pairs found under the given OCI directory.
///
/// The directory is expected to follow the layout created by `docker pull` or
/// this crate's [`RegistryClient`](crate::registry::RegistryClient), where
/// images are stored under `<name>/<tag>`.
pub fn collect_images<P: AsRef<Path>>(oci_dir: P) -> Result<Vec<String>> {
let base_dir = oci_dir.as_ref();
collect_image_directories(base_dir).map(|dirs| {
Expand All @@ -22,6 +27,8 @@ pub fn collect_images<P: AsRef<Path>>(oci_dir: P) -> Result<Vec<String>> {
})
}

/// Recursively walks the given path and collects directories that contain a
/// `manifest.json`, returning their absolute paths.
pub fn collect_image_directories<P: AsRef<Path>>(path: P) -> Result<Vec<PathBuf>> {
let mut result = Vec::new();
let path = path.as_ref();
Expand Down Expand Up @@ -51,6 +58,7 @@ fn collect_image_directories_recursive(path: &Path, result: &mut Vec<PathBuf>) -
Ok(())
}

/// Removes the directory storing the given `image:tag` from the OCI root.
pub fn delete_image<P: AsRef<Path>>(oci_dir: P, image: &str, tag: &str) -> Result<()> {
let image_path = oci_dir.as_ref().join(format!("{image}/{tag}"));
fs::remove_dir_all(&image_path)?;
Expand Down
11 changes: 11 additions & 0 deletions peeko/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
//! Core library for interacting with OCI container images that have been
//! downloaded to disk. The crate provides helpers for discovering images on the
//! filesystem, parsing image manifests, reading layer contents, downloading
//! artifacts from registries, and computing simple statistics about virtual
//! filesystems reconstructed from image layers.

/// Filesystem helpers for working with OCI image layouts stored on disk.
pub mod fs;
/// Types that model OCI image manifests and configs.
pub mod manifest;
/// Async readers that reconstruct a virtual filesystem view of image layers.
pub mod reader;
/// Clients for talking to OCI compatible registries.
pub mod registry;
/// Utilities for summarising reconstructed filesystem trees.
pub mod stats;
56 changes: 56 additions & 0 deletions peeko/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::collections::HashMap;

use serde::{Deserialize, Serialize};

/// High level representation of OCI manifest documents.
#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "mediaType")]
pub enum Manifest {
Expand All @@ -12,64 +13,92 @@ pub enum Manifest {
OCIIndex(ManifestList),
}

/// Representation of `application/vnd.oci.image.manifest.v1+json`.
#[derive(Debug, Deserialize, Serialize)]
pub struct ImageManifest {
#[serde(rename = "schemaVersion")]
/// Schema version declared by the manifest.
pub schema_version: u32,
#[serde(rename = "mediaType")]
/// Media type for the manifest.
pub media_type: String,
/// Descriptor that points to the configuration blob.
pub config: Descriptor,
/// Ordered layer descriptors composing the image.
pub layers: Vec<Descriptor>,

// for oci index
#[serde(skip_serializing_if = "Option::is_none")]
/// Optional annotations supplied by the image registry.
pub annotations: Option<HashMap<String, String>>,
}

/// Generic descriptor that points to a blob stored in the registry.
#[derive(Debug, Deserialize, Serialize)]
pub struct Descriptor {
/// SHA digest of the referenced blob.
pub digest: String,
#[serde(rename = "mediaType")]
/// Media type of the blob.
pub media_type: String,
/// Size in bytes of the blob.
pub size: u64,

// for oci index
#[serde(skip_serializing_if = "Option::is_none")]
/// Additional annotations provided by the registry.
pub annotations: Option<HashMap<String, String>>,
}

/// Representation of `application/vnd.oci.image.index.v1+json`.
#[derive(Debug, Deserialize, Serialize)]
pub struct ManifestList {
#[serde(rename = "schemaVersion")]
/// Schema version declared by the manifest.
pub schema_version: u32,
#[serde(rename = "mediaType")]
/// Media type for the manifest list.
pub media_type: String,
/// Architectures and platforms included in the manifest list.
pub manifests: Vec<PlatformManifest>,
}

/// Descriptor of a single platform entry inside an OCI index.
#[derive(Debug, Deserialize, Serialize)]
pub struct PlatformManifest {
/// SHA digest of the platform-specific manifest.
pub digest: String,
#[serde(rename = "mediaType")]
/// Media type for the manifest.
pub media_type: String,
/// Target platform described by this manifest.
pub platform: Platform,
/// Size in bytes of the manifest blob.
pub size: u64,
#[serde(skip_serializing_if = "Option::is_none")]
/// Optional annotations supplied by the registry.
pub annotations: Option<HashMap<String, String>>,
}

/// Platform information attached to a platform manifest descriptor.
#[derive(Debug, Deserialize, Serialize)]
pub struct Platform {
/// CPU architecture (for example `amd64` or `arm64`).
pub architecture: String,
/// Operating system (for example `linux`).
pub os: String,
#[serde(rename = "os.version", skip_serializing_if = "Option::is_none")]
/// Optional OS version.
pub os_version: Option<String>,
#[serde(rename = "os.features", skip_serializing_if = "Option::is_none")]
/// Optional OS feature list.
pub os_features: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
/// CPU variant (for example `v7`).
pub variant: Option<String>,
}

/// Returns the file extension associated with a descriptor's media type.
pub fn get_file_type(media_type: &str) -> &str {
match media_type.rsplit_once('+') {
Some((_, ext)) => ext,
Expand All @@ -80,67 +109,94 @@ pub fn get_file_type(media_type: &str) -> &str {
}
}

/// Runtime configuration extracted from an image config blob.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageConfig {
/// CPU architecture (for example `amd64` or `arm64`).
pub architecture: String,
/// Operating system (for example `linux`).
pub os: String,
/// Container runtime settings.
pub config: ContainerConfig,
/// Timestamp when the image was created.
pub created: String,
/// History describing how the image layers were produced.
pub history: Vec<HistoryEntry>,
/// Root filesystem diff IDs.
pub rootfs: RootFs,
}

/// Container runtime options section inside an image config.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerConfig {
#[serde(rename = "Hostname")]
/// Default hostname assigned to containers started from the image.
pub hostname: Option<String>,

#[serde(rename = "User")]
/// Default user (UID/GID) the container should run as.
pub user: Option<String>,

#[serde(rename = "Env")]
/// Default environment variables.
pub env: Option<Vec<String>>,

#[serde(rename = "Cmd")]
/// Default command executed by the container runtime.
pub cmd: Option<Vec<String>>,

#[serde(rename = "Entrypoint")]
/// Entrypoint process invoked before `Cmd`.
pub entrypoint: Option<Vec<String>>,

#[serde(rename = "WorkingDir")]
/// Working directory for the default command.
pub working_dir: Option<String>,

#[serde(rename = "Labels")]
/// Image labels attached to the runtime config.
pub labels: Option<HashMap<String, String>>,

#[serde(rename = "ExposedPorts")]
/// Ports exposed by default.
pub exposed_ports: Option<HashMap<String, serde_json::Value>>,

#[serde(rename = "Volumes")]
/// Named volumes declared by the image.
pub volumes: Option<HashMap<String, serde_json::Value>>,

#[serde(rename = "StopSignal")]
/// Signal used to request graceful shutdown.
pub stop_signal: Option<String>,

#[serde(rename = "Shell")]
/// Default shell used for command interpretation.
pub shell: Option<Vec<String>>,
}

/// Detailed history line for how an image layer was produced.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryEntry {
/// Timestamp when the layer was created.
pub created: String,
/// Command that produced the layer.
pub created_by: String,

#[serde(default)]
/// Whether the entry represents an empty layer.
pub empty_layer: bool,

#[serde(default)]
/// Optional comment attached to the history entry.
pub comment: Option<String>,
}

/// Root filesystem metadata inside an image config.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RootFs {
#[serde(rename = "type")]
/// Type of filesystem (typically `layers`).
pub fs_type: String,
/// Digest list representing layer diff IDs.
pub diff_ids: Vec<String>,
}
10 changes: 10 additions & 0 deletions peeko/src/reader/dir_tree.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
//! Utilities that model directory trees generated from OCI layers.

use std::cell::RefCell;
use std::collections::HashMap;
use std::path::Path;
use std::rc::{Rc, Weak};

/// Node within a directory tree backed by reference-counted pointers.
#[derive(Debug)]
pub struct TreeNode {
pub name: String,
Expand All @@ -12,6 +15,7 @@ pub struct TreeNode {
}

impl TreeNode {
/// Returns the full path for the node, optionally including the virtual root.
pub fn pwd(&self, with_root: bool) -> String {
let mut components = vec![self.name.clone()];
let mut current = self.parent.borrow().upgrade();
Expand All @@ -32,6 +36,7 @@ impl TreeNode {
}
}

/// Prints this node and its descendants up to `max_depth`.
pub fn print(&self, depth: usize, max_depth: usize, is_last: bool, prefix: &str) {
let new_prefix = if depth == 0 {
println!("{}", &self.name);
Expand Down Expand Up @@ -64,12 +69,14 @@ impl TreeNode {
}
}

/// Directory tree built from entries in a [`VirtualFileSystem`](super::vfs::VirtualFileSystem).
#[derive(Debug)]
pub struct DirectoryTree {
pub root: Rc<TreeNode>,
}

impl DirectoryTree {
/// Creates an empty directory tree with a root node named `/`.
pub fn new() -> Self {
Self {
root: Rc::new(TreeNode {
Expand All @@ -81,6 +88,7 @@ impl DirectoryTree {
}
}

/// Inserts a new path into the tree, creating intermediate directories as needed.
pub fn add_path<P: AsRef<Path>>(&self, path: P, is_dir: bool) {
let mut components: Vec<_> = path
.as_ref()
Expand Down Expand Up @@ -119,6 +127,7 @@ impl DirectoryTree {
}
}

/// Finds a node inside the tree by path returning a shared pointer to it.
pub fn find(&self, path: &str) -> Option<Rc<TreeNode>> {
if path.eq("/") {
return Some(Rc::clone(&self.root));
Expand All @@ -144,6 +153,7 @@ impl DirectoryTree {
Some(current)
}

/// Prints the entire tree to stdout up to `max_depth`.
pub fn print(&self, max_depth: usize) {
self.root.print(0, max_depth, true, "");
}
Expand Down
17 changes: 17 additions & 0 deletions peeko/src/reader/image_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use super::dir_tree::DirectoryTree;
use super::vfs::{FileEntry, VirtualFileSystem};
use crate::manifest::{ImageManifest, get_file_type};

/// Errors produced when building or using the asynchronous image reader.
#[derive(Error, Debug)]
pub enum ImageReaderError {
#[error("HTTP error: {0}")]
Expand All @@ -30,6 +31,7 @@ pub enum ImageReaderError {
NotAFile(String),
}

/// Convenient result alias that uses [`ImageReaderError`].
pub type Result<T> = std::result::Result<T, ImageReaderError>;

async fn load_manifest<P: AsRef<Path>>(image_dir: P) -> Result<ImageManifest> {
Expand Down Expand Up @@ -140,6 +142,11 @@ async fn read_file_from_layer<LP: AsRef<Path>, FP: AsRef<Path>>(
}
}

/// Constructs an `ImageReader` from an OCI image directory on disk.
///
/// The directory must contain a `manifest.json` and all layer blobs (named
/// `<digest>.<extension>`). Layers are replayed in order to build the virtual
/// filesystem that powers the reader.
pub async fn build_image_reader<P: AsRef<Path>>(image_dir: P) -> Result<ImageReader> {
let image_dir = image_dir.as_ref();
let manifest = load_manifest(image_dir).await?;
Expand All @@ -158,13 +165,17 @@ pub async fn build_image_reader<P: AsRef<Path>>(image_dir: P) -> Result<ImageRea
})
}

/// Provides filesystem-style access to an OCI image's layers and metadata.
pub struct ImageReader {
image_dir: PathBuf,
manifest: ImageManifest,
vfs: VirtualFileSystem,
}

impl ImageReader {
/// Reads the raw bytes of a file inside the reconstructed filesystem.
///
/// Returns an error when the path does not exist or addresses a directory.
pub async fn read_file<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
let path = path.as_ref();
let entry = self
Expand All @@ -186,11 +197,16 @@ impl ImageReader {
}
}

/// Builds an in-memory directory tree representing all files and
/// directories contained in the image.
pub fn get_dir_tree(&self) -> Result<DirectoryTree> {
let tree = self.vfs.get_directory_tree();
Ok(tree)
}

/// Prints the directory tree to stdout with an optional depth filter.
///
/// When `path` is supplied the tree is rooted at that subdirectory.
pub fn print_dir_tree(&self, depth: usize, path: Option<String>) -> Result<()> {
let tree = self.get_dir_tree()?;
let target_node = match &path {
Expand All @@ -209,6 +225,7 @@ impl ImageReader {
}
}

/// Returns metadata associated with a path in the virtual filesystem.
pub fn get_file_meatadata(&self, path: &str) -> Option<&FileEntry> {
self.vfs.get_entry(PathBuf::from(path))
}
Expand Down
4 changes: 4 additions & 0 deletions peeko/src/reader/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
//! Helpers for reconstructing filesystem content from OCI image layers.

mod archive_utils;
mod dir_tree;
mod image_reader;
pub mod vfs;

/// Error type returned by the asynchronous image reader.
pub use image_reader::ImageReaderError;
/// Build a high level image reader from an unpacked OCI image directory.
pub use image_reader::build_image_reader;
Loading