Skip to content
Open
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
2 changes: 2 additions & 0 deletions crates/cfsctl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ rhel9 = ['composefs/rhel9']
anyhow = { version = "1.0.87", default-features = false }
fn-error-context = "0.2"
clap = { version = "4.5.0", default-features = false, features = ["std", "help", "usage", "derive", "wrap_help"] }
comfy-table = { version = "7.1", default-features = false }
composefs = { workspace = true }
composefs-boot = { workspace = true }
composefs-oci = { workspace = true, optional = true }
composefs-http = { workspace = true, optional = true }
env_logger = { version = "0.11.0", default-features = false }
hex = { version = "0.4.0", default-features = false }
rustix = { version = "1.0.0", default-features = false, features = ["fs", "process"] }
serde_json = { version = "1.0", default-features = false, features = ["std"] }
tokio = { version = "1.24.2", default-features = false }

[lints]
Expand Down
145 changes: 86 additions & 59 deletions crates/cfsctl/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use std::{

use anyhow::Result;
use clap::{Parser, Subcommand, ValueEnum};
use comfy_table::{presets::UTF8_FULL, Table};

use rustix::fs::CWD;

Expand Down Expand Up @@ -103,12 +104,25 @@ enum OciCommand {
},
/// List all tagged OCI images in the repository
#[clap(name = "images")]
ListImages,
ListImages {
/// Output as JSON array
#[clap(long)]
json: bool,
},
/// Show information about an OCI image
///
/// By default, outputs JSON with manifest, config, and referrers.
/// Use --manifest or --config to output just that raw JSON.
#[clap(name = "inspect")]
Inspect {
/// Image reference (tag name or manifest digest)
image: String,
/// Output only the raw manifest JSON (as originally stored)
#[clap(long, conflicts_with = "config")]
manifest: bool,
/// Output only the raw config JSON (as originally stored)
#[clap(long, conflicts_with = "manifest")]
config: bool,
},
/// Tag an image with a new name
Tag {
Expand All @@ -122,6 +136,21 @@ enum OciCommand {
/// Tag name to remove
name: String,
},
/// Inspect a stored layer
///
/// By default, outputs the raw tar stream to stdout.
/// Use --dumpfile for composefs dumpfile format, or --json for metadata.
#[clap(name = "layer")]
LayerInspect {
/// Layer diff_id (sha256:...)
layer: String,
/// Output as composefs dumpfile format (one entry per line)
#[clap(long, conflicts_with = "json")]
dumpfile: bool,
/// Output layer metadata as JSON
#[clap(long, conflicts_with = "dumpfile")]
json: bool,
},
/// Compute the composefs image object id of the rootfs of a stored OCI image
ComputeId {
#[clap(flatten)]
Expand Down Expand Up @@ -390,16 +419,18 @@ where
println!("verity {}", result.manifest_verity.to_hex());
println!("tagged {tag_name}");
}
OciCommand::ListImages => {
OciCommand::ListImages { json } => {
let images = composefs_oci::oci_image::list_images(&repo)?;

if images.is_empty() {
if json {
println!("{}", serde_json::to_string_pretty(&images)?);
} else if images.is_empty() {
println!("No images found");
} else {
println!(
"{:<30} {:<12} {:<10} {:<8} {:<6}",
"NAME", "DIGEST", "ARCH", "SEALED", "LAYERS"
);
let mut table = Table::new();
table.load_preset(UTF8_FULL);
table.set_header(["NAME", "DIGEST", "ARCH", "SEALED", "LAYERS", "REFS"]);

for img in images {
let digest_short = img
.manifest_digest
Expand All @@ -410,68 +441,49 @@ where
} else {
digest_short
};
println!(
"{:<30} {:<12} {:<10} {:<8} {:<6}",
img.name,
let arch = if img.architecture.is_empty() {
"artifact"
} else {
&img.architecture
};
let sealed = if img.sealed { "yes" } else { "no" };
table.add_row([
img.name.as_str(),
digest_display,
if img.architecture.is_empty() {
"artifact"
} else {
&img.architecture
},
if img.sealed { "yes" } else { "no" },
img.layer_count
);
arch,
sealed,
&img.layer_count.to_string(),
&img.referrer_count.to_string(),
]);
}
println!("{table}");
}
}
OciCommand::Inspect { ref image } => {
OciCommand::Inspect {
ref image,
manifest,
config,
} => {
let img = if image.starts_with("sha256:") {
composefs_oci::oci_image::OciImage::open(&repo, image, None)?
} else {
composefs_oci::oci_image::OciImage::open_ref(&repo, image)?
};

println!("Manifest: {}", img.manifest_digest());
println!("Config: {}", img.config_digest());
println!(
"Type: {}",
if img.is_container_image() {
"container"
} else {
"artifact"
}
);

if img.is_container_image() {
println!("Architecture: {}", img.architecture());
println!("OS: {}", img.os());
}

if let Some(created) = img.created() {
println!("Created: {created}");
}

println!(
"Sealed: {}",
if img.is_sealed() { "yes" } else { "no" }
);
if let Some(seal) = img.seal_digest() {
println!("Seal digest: {seal}");
}

println!("Layers: {}", img.layer_descriptors().len());
for (i, layer) in img.layer_descriptors().iter().enumerate() {
println!(" [{i}] {} ({} bytes)", layer.digest(), layer.size());
}

if let Some(labels) = img.labels() {
if !labels.is_empty() {
println!("Labels:");
for (k, v) in labels {
println!(" {k}: {v}");
}
}
if manifest {
// Output raw manifest JSON exactly as stored
let manifest_json = img.read_manifest_json(&repo)?;
std::io::Write::write_all(&mut std::io::stdout(), &manifest_json)?;
println!();
} else if config {
// Output raw config JSON exactly as stored
let config_json = img.read_config_json(&repo)?;
std::io::Write::write_all(&mut std::io::stdout(), &config_json)?;
println!();
} else {
// Default: output combined JSON with manifest, config, and referrers
let output = img.inspect_json(&repo)?;
println!("{}", serde_json::to_string_pretty(&output)?);
}
}
OciCommand::Tag {
Expand All @@ -485,6 +497,21 @@ where
composefs_oci::oci_image::untag_image(&repo, name)?;
println!("Removed tag {name}");
}
OciCommand::LayerInspect {
ref layer,
dumpfile,
json,
} => {
if json {
let info = composefs_oci::layer_info(&repo, layer)?;
println!("{}", serde_json::to_string_pretty(&info)?);
} else if dumpfile {
composefs_oci::layer_dumpfile(&repo, layer, &mut std::io::stdout())?;
} else {
// Default: output raw tar
composefs_oci::layer_tar(&repo, layer, &mut std::io::stdout())?;
Comment on lines +508 to +512
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we care whether the output is a tty?

}
}
OciCommand::Seal {
config_opts:
OCIConfigOptions {
Expand Down
2 changes: 2 additions & 0 deletions crates/composefs-oci/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ hex = { version = "0.4.0", default-features = false }
indicatif = { version = "0.17.0", default-features = false, features = ["tokio"] }
oci-spec = { version = "0.8.0", default-features = false }
rustix = { version = "1.0.0", features = ["fs"] }
serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_json = { version = "1.0", default-features = false, features = ["std"] }
sha2 = { version = "0.10.1", default-features = false }
tar = { version = "0.4.38", default-features = false }
tokio = { version = "1.24.2", features = ["rt-multi-thread"] }
Expand Down
2 changes: 1 addition & 1 deletion crates/composefs-oci/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ mod test {
fsverity::Sha256HashValue,
tree::{LeafContent, RegularFile, Stat},
};
use std::{cell::RefCell, collections::BTreeMap, io::BufRead, io::Read, path::PathBuf};
use std::{cell::RefCell, collections::BTreeMap, io::BufRead, path::PathBuf};

use super::*;

Expand Down
6 changes: 3 additions & 3 deletions crates/composefs-oci/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ use crate::tar::get_entry;

// Re-export key types for convenience
pub use oci_image::{
add_referrer, list_images, list_referrers, list_refs, remove_referrer,
remove_referrers_for_subject, resolve_ref, tag_image, untag_image, ImageInfo, OciImage,
OCI_REF_PREFIX,
add_referrer, layer_dumpfile, layer_info, layer_tar, list_images, list_referrers, list_refs,
remove_referrer, remove_referrers_for_subject, resolve_ref, tag_image, untag_image, ImageInfo,
LayerInfo, OciImage, SplitstreamInfo, OCI_REF_PREFIX,
};
pub use skopeo::{pull_image, PullResult};

Expand Down
Loading
Loading