Skip to content

Commit

Permalink
Support applying layers to disk
Browse files Browse the repository at this point in the history
  • Loading branch information
jssblck committed Dec 11, 2024
1 parent eda948e commit 38af208
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 84 deletions.
18 changes: 6 additions & 12 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
/target

fossa.debug.json
fossa.debug.json.gz
fossa.telemetry.json
fossa.telemetry.json.gz

# Added by cargo

/target
scratch/
61 changes: 0 additions & 61 deletions bin/src/cmd_extract.rs

This file was deleted.

116 changes: 116 additions & 0 deletions bin/src/extract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use circe::{registry::Registry, Platform, Reference};
use clap::{Parser, ValueEnum};
use color_eyre::eyre::{bail, Context, Result};
use std::{path::PathBuf, str::FromStr};
use tracing::info;

#[derive(Debug, Parser)]
pub struct Options {
/// Image reference being extracted (e.g. docker.io/library/ubuntu:latest)
#[arg(value_parser = Reference::from_str)]
image: Reference,

/// Directory to which the extracted contents will be written
#[arg(default_value = ".")]
output_dir: String,

/// Overwrite the existing output directory if it exists.
#[arg(long, short)]
overwrite: bool,

/// Platform to extract (e.g. linux/amd64)
///
/// If the image is not multi-platform, this is ignored.
/// If the image is multi-platform, this is used to select the platform to extract.
///
/// If the image is multi-platform and this argument is not provided,
/// the platform is chosen according to the following priority list:
///
/// 1. The first platform-independent image
///
/// 2. The current platform (if available)
///
/// 3. The `linux` platform for the current architecture
///
/// 4. The `linux` platform for the `amd64` architecture
///
/// 5. The first platform in the image manifest
#[arg(long, value_parser = Platform::from_str)]
platform: Option<Platform>,

/// How to handle layers during extraction
#[arg(long, default_value = "squash")]
mode: Mode,
}

#[derive(Copy, Clone, Debug, Default, ValueEnum)]
pub enum Mode {
/// Squash all layers into a single output
///
/// This results in the output directory containing the same equivalent file system
/// as if the container was actually booted.
#[default]
Squash,
}

#[tracing::instrument]
pub async fn main(opts: Options) -> Result<()> {
info!("Extracting image");

let output = canonicalize_output_dir(&opts.output_dir, opts.overwrite)?;
let registry = Registry::builder()
.maybe_platform(opts.platform)
.reference(opts.image)
.build()
.await
.context("configure remote registry")?;

let layers = registry.layers().await.context("list layers")?;
let count = layers.len();
info!("enumerated {count} {}", plural(count, "layer", "layers"));

for (descriptor, layer) in layers.into_iter().zip(1usize..) {
info!(layer = %descriptor, "applying layer {layer} of {count}");
registry
.apply_layer(&descriptor, &output)
.await
.with_context(|| format!("apply layer {descriptor} to {output:?}"))?;
}

Ok(())
}

/// Given a (probably relative) path to a directory, canonicalize it to an absolute path.
/// If the path already exists, behavior depends on the `overwrite` flag:
/// - If `overwrite` is true, the existing directory is removed and a new one is created.
/// - If `overwrite` is false, an error is returned.
fn canonicalize_output_dir(path: &str, overwrite: bool) -> Result<PathBuf> {
let path = PathBuf::from(path);

// If we're able to canonicalize the path, it already exists.
// We want to remove its contents and recreate it if `overwrite` is true.
if let Ok(path) = std::fs::canonicalize(&path) {
if !overwrite {
bail!("output directory already exists: {path:?}");
}

info!(?path, "removing existing output directory");
std::fs::remove_dir_all(&path).context("remove existing output directory")?;
std::fs::create_dir(&path).context("create new directory")?;
return Ok(path);
}

// Failed to canonicalize the path, which means it doesn't exist.
// We need to create it, then canonicalize it now that it exists.
info!(?path, "creating new output directory");
std::fs::create_dir_all(&path).context("create parent dir")?;
std::fs::canonicalize(&path).context("canonicalize path")
}

fn plural<'a>(count: usize, singular: &'a str, plural: &'a str) -> &'a str {
if count == 1 {
singular
} else {
plural
}
}
6 changes: 3 additions & 3 deletions bin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use color_eyre::eyre::Result;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{self, prelude::*};

mod cmd_extract;
mod extract;

#[derive(Debug, Parser)]
#[command(author, version, about)]
Expand All @@ -15,7 +15,7 @@ struct Cli {
#[derive(Debug, Parser)]
enum Commands {
/// Extract OCI image to a directory
Extract(cmd_extract::Options),
Extract(extract::Options),
}

#[tokio::main]
Expand Down Expand Up @@ -44,7 +44,7 @@ async fn main() -> Result<()> {

let cli = Cli::parse();
match cli.command {
Commands::Extract(opts) => cmd_extract::main(opts).await?,
Commands::Extract(opts) => extract::main(opts).await?,
}

Ok(())
Expand Down
1 change: 1 addition & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ hex = "0.4.3"
hex-magic = "0.0.2"
itertools = "0.13.0"
oci-client = "0.14.0"
os_str_bytes = "7.0.0"
static_assertions = "1.1.0"
strum = { version = "0.26.3", features = ["derive"] }
tap = "1.0.1"
Expand Down
31 changes: 31 additions & 0 deletions lib/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,34 @@ fn priority_find<T, F: Fn(&T) -> usize>(
})
.map(|(_, item)| item)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn priority_find_basic() {
let items = vec!["a", "b", "c", "d", "e"];
let result = priority_find(items, |item| match *item {
"a" => 2,
"b" => 1,
"d" => 0,
_ => 3,
});
assert_eq!(result, Some("d"));
}

#[test]
fn priority_find_all_zero() {
let items = vec!["a", "b", "c", "d", "e"];
let result = priority_find(items, |_| 0);
assert_eq!(result, Some("a"));
}

#[test]
fn priority_find_empty() {
let items = Vec::<&str>::new();
let result = priority_find(items, |_| 0);
assert_eq!(result, None);
}
}
3 changes: 2 additions & 1 deletion lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use color_eyre::{
eyre::{self, bail, eyre, Context},
Result, Section, SectionExt,
};
use derive_more::derive::Display;
use derive_more::derive::{Debug, Display};
use itertools::Itertools;
use std::str::FromStr;
use strum::{AsRefStr, EnumIter, IntoEnumIterator};
Expand Down Expand Up @@ -244,6 +244,7 @@ macro_rules! digest {
/// assert_eq!(digest.as_hex(), "a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4");
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
#[debug("{}", self.to_string())]
pub struct Digest {
/// The hashing algorithm used (e.g. "sha256")
pub algorithm: String,
Expand Down
Loading

0 comments on commit 38af208

Please sign in to comment.