Skip to content

Commit

Permalink
Add list subcommand
Browse files Browse the repository at this point in the history
  • Loading branch information
jssblck committed Dec 12, 2024
1 parent d4bad05 commit 956235a
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 49 deletions.
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ powershell -c "irm https://github.com/fossas/circe/releases/latest/download/circ
> [!TIP]
> Check the help output for more details.
## extract

Extracts the contents of the image to disk.

```shell
# Export the contents of the image to disk.
# Extracts the contents of the image to disk.
#
# Usage:
# circe extract <image> <target> [--layers <layers>] [--platform <platform>] [--overwrite]
Expand All @@ -58,13 +62,50 @@ powershell -c "irm https://github.com/fossas/circe/releases/latest/download/circ
# Accepts the same values as `docker` (e.g. `linux/amd64`, `darwin/arm64`, etc).
# --overwrite
# If the target directory already exists, overwrite it.
# --layer-glob, --lg
# A glob pattern to filter layers to extract.
# Layers matching this pattern are extracted.
# --layer-regex, --lr
# A regex pattern to filter layers to extract.
# Layers matching this pattern are extracted.
# --file-glob, --fg
# A glob pattern to filter files to extract.
# Files matching this pattern are extracted.
# --file-regex, --fr
# A regex pattern to filter files to extract.
# Files matching this pattern are extracted.
# --username
# The username to use for authentication; "password" is also required if provided.
# --password
# The password to use for authentication; "username" is also required if provided.
circe extract docker.io/contribsys/faktory:latest ./faktory --layers squash --platform linux/amd64
```

## list

Lists the contents of an image.

```shell
# Lists the contents of the image.
#
# Usage:
# circe list <image> [--platform <platform>] [--username <username>] [--password <password>]
#
# Arguments:
# <image>
# The image to list.
#
# Options for `circe list`:
# --platform
# Defaults to your current platform.
# Accepts the same values as `docker` (e.g. `linux/amd64`, `darwin/arm64`, etc).
# --username
# The username to use for authentication; "password" is also required if provided.
# --password
# The password to use for authentication; "username" is also required if provided.
circe list docker.io/contribsys/faktory:latest
```

## platform selection

You can customize the platform used by `circe` by passing `--platform`.
Expand Down
3 changes: 2 additions & 1 deletion bin/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "circe"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
authors = ["Jessica Black <[email protected]>", "FOSSA Inc. <[email protected]>"]
description = "Extracts and examines the contents of containers"
Expand All @@ -24,3 +24,4 @@ tracing-tree = { version = "0.4.0" }
circe_lib = { path = "../lib" }
serde_json = "1.0.133"
derive_more = { version = "1.0.0", features = ["debug"] }
pluralizer = "0.4.0"
56 changes: 32 additions & 24 deletions bin/src/extract.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
use circe_lib::{
registry::Registry, Authentication, Filters, LayerDescriptor, Platform, Reference,
};
use clap::{Parser, ValueEnum};
use clap::{Args, Parser, ValueEnum};
use color_eyre::eyre::{bail, Context, Result};
use derive_more::Debug;
use std::{path::PathBuf, str::FromStr};
use tracing::{debug, 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,
/// Target to extract
#[clap(flatten)]
target: Target,

/// Directory to which the extracted contents will be written
#[arg(default_value = ".")]
Expand All @@ -21,21 +21,6 @@ pub struct Options {
#[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, verbatim_doc_comment)]
platform: Option<Platform>,

/// How to handle layers during extraction
#[arg(long, default_value = "squash")]
layers: Mode,
Expand Down Expand Up @@ -88,15 +73,38 @@ pub struct Options {
/// If filters are provided, only files whose path matches any filter are extracted.
#[arg(long, alias = "fr")]
file_regex: Option<Vec<String>>,
}

/// Shared options for any command that needs to work with the OCI registry for a given image.
#[derive(Debug, Args)]
pub struct Target {
/// Image reference being extracted (e.g. docker.io/library/ubuntu:latest)
#[arg(value_parser = Reference::from_str)]
pub image: Reference,

/// 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, verbatim_doc_comment)]
pub platform: Option<Platform>,

/// The username to use for authenticating to the registry
#[arg(long, requires = "password")]
username: Option<String>,
pub username: Option<String>,

/// The password to use for authenticating to the registry
#[arg(long, requires = "username")]
#[debug(skip)]
password: Option<String>,
pub password: Option<String>,
}

#[derive(Copy, Clone, Debug, Default, ValueEnum)]
Expand All @@ -120,7 +128,7 @@ pub enum Mode {
pub async fn main(opts: Options) -> Result<()> {
info!("extracting image");

let auth = match (opts.username, opts.password) {
let auth = match (opts.target.username, opts.target.password) {
(Some(username), Some(password)) => Authentication::basic(username, password),
_ => Authentication::default(),
};
Expand All @@ -132,8 +140,8 @@ pub async fn main(opts: Options) -> Result<()> {

let output = canonicalize_output_dir(&opts.output_dir, opts.overwrite)?;
let registry = Registry::builder()
.maybe_platform(opts.platform)
.reference(opts.image)
.maybe_platform(opts.target.platform)
.reference(opts.target.image)
.auth(auth)
.layer_filters(layer_globs + layer_regexes)
.file_filters(file_globs + file_regexes)
Expand Down
54 changes: 54 additions & 0 deletions bin/src/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use circe_lib::{registry::Registry, Authentication};
use clap::Parser;
use color_eyre::eyre::{Context, Result};
use derive_more::Debug;
use pluralizer::pluralize;
use std::collections::HashMap;
use tracing::{debug, info};

use crate::extract::Target;

#[derive(Debug, Parser)]
pub struct Options {
/// Target to list
#[clap(flatten)]
target: Target,
}

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

let auth = match (opts.target.username, opts.target.password) {
(Some(username), Some(password)) => Authentication::basic(username, password),
_ => Authentication::default(),
};
let registry = Registry::builder()
.maybe_platform(opts.target.platform)
.reference(opts.target.image)
.auth(auth)
.build()
.await
.context("configure remote registry")?;

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

let mut listing = HashMap::new();
for (descriptor, layer) in layers.into_iter().zip(1usize..) {
info!(layer = %descriptor, %layer, "reading layer");
let files = registry
.list_files(&descriptor)
.await
.context("list files")?;

debug!(layer = %descriptor, files = %files.len(), "listed files");
listing.insert(descriptor.digest.to_string(), files);
}

let rendered = serde_json::to_string_pretty(&listing).context("render listing")?;
println!("{rendered}");

Ok(())
}
6 changes: 5 additions & 1 deletion bin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use tracing::level_filters::LevelFilter;
use tracing_subscriber::{self, prelude::*};

mod extract;

mod list;
#[derive(Debug, Parser)]
#[command(author, version, about)]
struct Cli {
Expand All @@ -16,6 +16,9 @@ struct Cli {
enum Commands {
/// Extract OCI image to a directory
Extract(extract::Options),

/// Enumerate the layers and files in an OCI image
List(list::Options),
}

#[tokio::main]
Expand Down Expand Up @@ -46,6 +49,7 @@ async fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Extract(opts) => extract::main(opts).await?,
Commands::List(opts) => list::main(opts).await?,
}

Ok(())
Expand Down
2 changes: 1 addition & 1 deletion lib/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "circe_lib"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
authors = ["Jessica Black <[email protected]>", "FOSSA Inc. <[email protected]>"]
description = "Extracts and examines the contents of containers"
Expand Down
Loading

0 comments on commit 956235a

Please sign in to comment.