From ee5d36ff36a217fe966fd24bf66eda25ad74f5bd Mon Sep 17 00:00:00 2001 From: Allison Karlitskaya Date: Mon, 5 May 2025 23:13:23 +0200 Subject: [PATCH 1/8] Cargo.toml: switch to a workspace Split into a few separate crates: - libraries: - composefs - composefs-oci - composefs-boot - binaries: - cfsctl - composefs-setup-root - erofs-debug Move our lint config (which only forbids missing debug impls) to the workspace level and have all crates inherit from that. Add a new workflow for testing that we can `cargo package` everything. We need a nightly cargo in order to do this with workspaces containing inter-dependent crates: https://github.com/rust-lang/cargo/pull/13947 Make 'oci' an optional feature of cfsctl, but enable it by default. Adjust our rawhide bls example (which included --no-default-features) to *not* disable that. This is not a huge improvement in terms of compile speed, and it has some drawbacks (like 'cargo run' no longer defaulting to cfsctl) but it seems like the right step at this point. I want to start to add some more experimental code without making it part of the main crate. Signed-off-by: Allison Karlitskaya --- Cargo.toml | 31 +++++ src/main.rs | 349 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 380 insertions(+) create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..56696f51c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "cfsctl" +description = "Command-line utility for composefs" +default-run = "cfsctl" + +edition.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[features] +default = ['pre-6.15', 'oci'] +oci = ['composefs-oci'] +rhel9 = ['composefs/rhel9'] +'pre-6.15' = ['composefs/pre-6.15'] + +[dependencies] +anyhow = { version = "1.0.87", default-features = false } +clap = { version = "4.0.1", default-features = false, features = ["std", "help", "usage", "derive"] } +composefs = { workspace = true } +composefs-boot = { workspace = true } +composefs-oci = { 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"] } +tokio = { version = "1.24.2", default-features = false } + +[lints] +workspace = true diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 000000000..c0e323692 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,349 @@ +use std::{ + fs::create_dir_all, + path::{Path, PathBuf}, + sync::Arc, +}; + +use anyhow::Result; +use clap::{Parser, Subcommand}; + +use rustix::fs::CWD; + +use composefs_boot::{write_boot, BootOps}; + +use composefs::{ + fsverity::{FsVerityHashValue, Sha256HashValue}, + repository::Repository, +}; + +/// cfsctl +#[derive(Debug, Parser)] +#[clap(name = "cfsctl", version)] +pub struct App { + #[clap(long, group = "repopath")] + repo: Option, + #[clap(long, group = "repopath")] + user: bool, + #[clap(long, group = "repopath")] + system: bool, + + #[clap(subcommand)] + cmd: Command, +} + +#[cfg(feature = "oci")] +#[derive(Debug, Subcommand)] +enum OciCommand { + /// Stores a tar file as a splitstream in the repository. + ImportLayer { + sha256: String, + name: Option, + }, + /// Lists the contents of a tar stream + LsLayer { + /// the name of the stream + name: String, + }, + Dump { + config_name: String, + config_verity: Option, + }, + Pull { + image: String, + name: Option, + }, + ComputeId { + config_name: String, + config_verity: Option, + #[clap(long)] + bootable: bool, + }, + CreateImage { + config_name: String, + config_verity: Option, + #[clap(long)] + bootable: bool, + #[clap(long)] + image_name: Option, + }, + Seal { + config_name: String, + config_verity: Option, + }, + Mount { + name: String, + mountpoint: String, + }, + PrepareBoot { + config_name: String, + config_verity: Option, + #[clap(long, default_value = "/boot")] + bootdir: PathBuf, + #[clap(long)] + entry_id: Option, + #[clap(long)] + cmdline: Vec, + }, +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Take a transaction lock on the repository. + /// This prevents garbage collection from occurring. + Transaction, + /// Reconstitutes a split stream and writes it to stdout + Cat { + /// the name of the stream to cat, either a sha256 digest or prefixed with 'ref/' + name: String, + }, + /// Perform garbage collection + GC, + /// Imports a composefs image (unsafe!) + ImportImage { + reference: String, + }, + /// Commands for dealing with OCI layers + #[cfg(feature = "oci")] + Oci { + #[clap(subcommand)] + cmd: OciCommand, + }, + /// Mounts a composefs, possibly enforcing fsverity of the image + Mount { + /// the name of the image to mount, either a sha256 digest or prefixed with 'ref/' + name: String, + /// the mountpoint + mountpoint: String, + }, + CreateImage { + path: PathBuf, + #[clap(long)] + bootable: bool, + #[clap(long)] + stat_root: bool, + image_name: Option, + }, + ComputeId { + path: PathBuf, + #[clap(long)] + bootable: bool, + #[clap(long)] + stat_root: bool, + }, + CreateDumpfile { + path: PathBuf, + #[clap(long)] + bootable: bool, + #[clap(long)] + stat_root: bool, + }, + ImageObjects { + name: String, + }, +} + +fn verity_opt(opt: &Option) -> Result> { + Ok(match opt { + Some(value) => Some(FsVerityHashValue::from_hex(value)?), + None => None, + }) +} + +#[tokio::main] +async fn main() -> Result<()> { + env_logger::init(); + + let args = App::parse(); + + let repo: Repository = (if let Some(path) = &args.repo { + Repository::open_path(CWD, path) + } else if args.system { + Repository::open_system() + } else if args.user { + Repository::open_user() + } else if rustix::process::getuid().is_root() { + Repository::open_system() + } else { + Repository::open_user() + })?; + + match args.cmd { + Command::Transaction => { + // just wait for ^C + loop { + std::thread::park(); + } + } + Command::Cat { name } => { + repo.merge_splitstream(&name, None, &mut std::io::stdout())?; + } + Command::ImportImage { reference } => { + let image_id = repo.import_image(&reference, &mut std::io::stdin())?; + println!("{}", image_id.to_id()); + } + #[cfg(feature = "oci")] + Command::Oci { cmd: oci_cmd } => match oci_cmd { + OciCommand::ImportLayer { name, sha256 } => { + let object_id = composefs_oci::import_layer( + &Arc::new(repo), + &composefs::util::parse_sha256(sha256)?, + name.as_deref(), + &mut std::io::stdin(), + )?; + println!("{}", object_id.to_id()); + } + OciCommand::LsLayer { name } => { + composefs_oci::ls_layer(&repo, &name)?; + } + OciCommand::Dump { + ref config_name, + ref config_verity, + } => { + let verity = verity_opt(config_verity)?; + let mut fs = + composefs_oci::image::create_filesystem(&repo, config_name, verity.as_ref())?; + fs.print_dumpfile()?; + } + OciCommand::ComputeId { + ref config_name, + ref config_verity, + bootable, + } => { + let verity = verity_opt(config_verity)?; + let mut fs = + composefs_oci::image::create_filesystem(&repo, config_name, verity.as_ref())?; + if bootable { + fs.transform_for_boot(&repo)?; + } + let id = fs.compute_image_id(); + println!("{}", id.to_hex()); + } + OciCommand::CreateImage { + ref config_name, + ref config_verity, + bootable, + ref image_name, + } => { + let verity = verity_opt(config_verity)?; + let mut fs = + composefs_oci::image::create_filesystem(&repo, config_name, verity.as_ref())?; + if bootable { + fs.transform_for_boot(&repo)?; + } + let image_id = fs.commit_image(&repo, image_name.as_deref())?; + println!("{}", image_id.to_id()); + } + OciCommand::Pull { ref image, name } => { + let (sha256, verity) = + composefs_oci::pull(&Arc::new(repo), image, name.as_deref()).await?; + + println!("sha256 {}", hex::encode(sha256)); + println!("verity {}", verity.to_hex()); + } + OciCommand::Seal { + ref config_name, + ref config_verity, + } => { + let verity = verity_opt(config_verity)?; + let (sha256, verity) = + composefs_oci::seal(&Arc::new(repo), config_name, verity.as_ref())?; + println!("sha256 {}", hex::encode(sha256)); + println!("verity {}", verity.to_id()); + } + OciCommand::Mount { + ref name, + ref mountpoint, + } => { + composefs_oci::mount(&repo, name, mountpoint, None)?; + } + OciCommand::PrepareBoot { + ref config_name, + ref config_verity, + ref bootdir, + ref entry_id, + ref cmdline, + } => { + let verity = verity_opt(config_verity)?; + let mut fs = + composefs_oci::image::create_filesystem(&repo, config_name, verity.as_ref())?; + let entries = fs.transform_for_boot(&repo)?; + let id = fs.commit_image(&repo, None)?; + + let Some(entry) = entries.into_iter().next() else { + anyhow::bail!("No boot entries!"); + }; + + let cmdline_refs: Vec<&str> = cmdline.iter().map(String::as_str).collect(); + write_boot::write_boot_simple( + &repo, + entry, + &id, + bootdir, + entry_id.as_deref(), + &cmdline_refs, + )?; + + let state = args + .repo + .as_ref() + .map(|p: &PathBuf| p.parent().unwrap()) + .unwrap_or(Path::new("/sysroot")) + .join("state") + .join(id.to_hex()); + + create_dir_all(state.join("var"))?; + create_dir_all(state.join("etc/upper"))?; + create_dir_all(state.join("etc/work"))?; + } + }, + Command::ComputeId { + ref path, + bootable, + stat_root, + } => { + let mut fs = composefs::fs::read_filesystem(CWD, path, Some(&repo), stat_root)?; + if bootable { + fs.transform_for_boot(&repo)?; + } + let id = fs.compute_image_id(); + println!("{}", id.to_hex()); + } + Command::CreateImage { + ref path, + bootable, + stat_root, + ref image_name, + } => { + let mut fs = composefs::fs::read_filesystem(CWD, path, Some(&repo), stat_root)?; + if bootable { + fs.transform_for_boot(&repo)?; + } + let id = fs.commit_image(&repo, image_name.as_deref())?; + println!("{}", id.to_id()); + } + Command::CreateDumpfile { + ref path, + bootable, + stat_root, + } => { + let mut fs = composefs::fs::read_filesystem(CWD, path, Some(&repo), stat_root)?; + if bootable { + fs.transform_for_boot(&repo)?; + } + fs.print_dumpfile()?; + } + Command::Mount { name, mountpoint } => { + repo.mount(&name, &mountpoint)?; + } + Command::ImageObjects { name } => { + let objects = repo.objects_for_image(&name)?; + for object in objects { + println!("{}", object.to_id()); + } + } + Command::GC => { + repo.gc()?; + } + } + Ok(()) +} From e2e309864e8cc42ecc1d0951821f7d85dcbe0ded Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Fri, 9 May 2025 16:18:18 +0530 Subject: [PATCH 2/8] Allow passing absolute paths to initrd and vmlinuz Grub needs absolute paths to initrd and vmlinuz if we do not have `/boot` in a boot partition, which we do not in bootc. Add param `boot_subdir` which acts like a subdirectory in the boot directory in case the boot partition is mounted in another directory. Signed-off-by: Pragyan Poudyal --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index c0e323692..d853dbf14 100644 --- a/src/main.rs +++ b/src/main.rs @@ -279,6 +279,7 @@ async fn main() -> Result<()> { entry, &id, bootdir, + None, entry_id.as_deref(), &cmdline_refs, )?; From 8051480f17176b38a46b5bd378ec03a6959b7ded Mon Sep 17 00:00:00 2001 From: Allison Karlitskaya Date: Thu, 24 Apr 2025 21:11:35 +0200 Subject: [PATCH 3/8] src: experimental http client support It turns out that the information contained in splitstreams to assist with garbage collection (ie: the list of things that we mustn't discard) is exactly the required information for downloading (ie: the list of things that we must acquire). Use this fact to add support for fetching repository content from HTTP servers. We only download the objects that are actually required, so incremental pulls are very fast. This works with just about any HTTP server, so you can do something like python -m http.server -d ~/.var/lib/composefs and download from that. With a fast enough web server on localhost, pulling a complete image into an empty repository takes about as long as pulling an `oci:` directory via skopeo with `cfsctl oci pull`. In practice, this is intended to be used with a webserver which supports static compression and pre-compressed objects stored on the server. In particular, zstd support is enabled in the `reqwest` crate for this reason, and it's working with something like: find repo/objects/ -type f -name '*[0-9a-f]' -exec zstd -19 -v '{}' + static-web-server -p 8888 --compression-static -d repo There's also an included s3-uploader.py in the examples/ directory which will upload a repository to an S3 bucket, with zstd compression. Signed-off-by: Allison Karlitskaya --- Cargo.toml | 2 ++ src/main.rs | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 56696f51c..4901f490f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ version.workspace = true [features] default = ['pre-6.15', 'oci'] +http = ['composefs-http'] oci = ['composefs-oci'] rhel9 = ['composefs/rhel9'] 'pre-6.15' = ['composefs/pre-6.15'] @@ -22,6 +23,7 @@ clap = { version = "4.0.1", default-features = false, features = ["std", "help", 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"] } diff --git a/src/main.rs b/src/main.rs index d853dbf14..e64953352 100644 --- a/src/main.rs +++ b/src/main.rs @@ -140,6 +140,11 @@ enum Command { ImageObjects { name: String, }, + #[cfg(feature = "http")] + Fetch { + url: String, + name: String, + }, } fn verity_opt(opt: &Option) -> Result> { @@ -345,6 +350,12 @@ async fn main() -> Result<()> { Command::GC => { repo.gc()?; } + #[cfg(feature = "http")] + Command::Fetch { url, name } => { + let (sha256, verity) = composefs_http::download(&url, &name, Arc::new(repo)).await?; + println!("sha256 {}", hex::encode(sha256)); + println!("verity {}", verity.to_hex()); + } } Ok(()) } From 3e08695cf80ad6c29a7d1ea77293eb2e0b316622 Mon Sep 17 00:00:00 2001 From: Allison Karlitskaya Date: Tue, 24 Jun 2025 12:43:19 +0200 Subject: [PATCH 4/8] mount: clean up mount APIs Change the Repository::mount() API to return the mounted filesystem as an fd rather than taking the mountpoint as an argument. Create a new mount_at() API to replace the old one, replacing the canicalize() and mount_at() calls that used to be in mount_composefs_at(), which we remove. Update the various users. Making this change lets us simplify the logic in composefs-setup-root: it no longer has to manually open the image in order to perform the fsmount operation: it can use the new API on the repository. This allows us to make Repository::open_image() private, so do that too. Co-authored-by: Sanne Raymaekers Signed-off-by: Allison Karlitskaya --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index e64953352..eb0c4c09e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -339,7 +339,7 @@ async fn main() -> Result<()> { fs.print_dumpfile()?; } Command::Mount { name, mountpoint } => { - repo.mount(&name, &mountpoint)?; + repo.mount_at(&name, &mountpoint)?; } Command::ImageObjects { name } => { let objects = repo.objects_for_image(&name)?; From ae3b538c93d96bd4bf6941496f9ec0e76adbe387 Mon Sep 17 00:00:00 2001 From: Sanne Raymaekers Date: Sat, 14 Jun 2025 13:29:41 +0200 Subject: [PATCH 5/8] cfsctl: add insecure option Allows cfsctl operations with fs-verity disabled. Signed-off-by: Sanne Raymaekers --- src/main.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index eb0c4c09e..03100ec83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,12 @@ pub struct App { #[clap(long, group = "repopath")] system: bool, + /// Sets the repository to insecure before running any operation and + /// prepend '?' to the composefs kernel command line when writing + /// boot entry. + #[clap(long)] + insecure: bool, + #[clap(subcommand)] cmd: Command, } @@ -160,7 +166,7 @@ async fn main() -> Result<()> { let args = App::parse(); - let repo: Repository = (if let Some(path) = &args.repo { + let mut repo: Repository = (if let Some(path) = &args.repo { Repository::open_path(CWD, path) } else if args.system { Repository::open_system() @@ -172,6 +178,8 @@ async fn main() -> Result<()> { Repository::open_user() })?; + repo.set_insecure(args.insecure); + match args.cmd { Command::Transaction => { // just wait for ^C From f06a96c00a887ed83af763666b36262aab77192b Mon Sep 17 00:00:00 2001 From: Sanne Raymaekers Date: Wed, 18 Jun 2025 15:57:17 +0200 Subject: [PATCH 6/8] composefs-boot: support writing insecure composefs cmdline Supports writing `composefs=?`. Signed-off-by: Sanne Raymaekers --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index 03100ec83..516c8b32f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -291,6 +291,7 @@ async fn main() -> Result<()> { &repo, entry, &id, + args.insecure, bootdir, None, entry_id.as_deref(), From b46b1f781e0d963b9cd340f1bdda9ca28c985b06 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 25 Jun 2025 15:27:22 +0530 Subject: [PATCH 7/8] Move state to `/state/deploy` Signed-off-by: Pragyan Poudyal --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 516c8b32f..dd66a949f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -303,7 +303,7 @@ async fn main() -> Result<()> { .as_ref() .map(|p: &PathBuf| p.parent().unwrap()) .unwrap_or(Path::new("/sysroot")) - .join("state") + .join("state/deploy") .join(id.to_hex()); create_dir_all(state.join("var"))?; From 9d3ccd048cbb2211a2fae592bd90e9e7a29b8c33 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 31 Jul 2025 17:20:33 -0400 Subject: [PATCH 8/8] Add `bootc internals cfs` This exposes the current functionality of the cfsctl binary. It's not a crate right now, and it's not a lot of code, so we just fork it. I did take the effort to use `git subtree merge` to do the import. For the record, here's how I did it: - In composefs-rs: git subtree split --prefix=crates/cfsctl - In bootc: git subtree add --prefix=crates/lib/cfsctl ../../containers/composefs-rs/ In cfsctl I also: - Adjusted it to accept the bootc-configured composefs repo (which note is right now hardcoded to sha512, not sha256) - Dropped the http stuff since I don't think it really makes sense vs OCI Signed-off-by: Colin Walters --- Cargo.lock | 3 + crates/lib/Cargo.toml | 3 + crates/lib/cfsctl/Cargo.toml | 33 --------- .../lib/{cfsctl/src/main.rs => src/cfsctl.rs} | 74 ++++++++----------- crates/lib/src/cli.rs | 9 +++ crates/lib/src/lib.rs | 1 + .../booted/readonly/030-test-composefs.nu | 4 + 7 files changed, 52 insertions(+), 75 deletions(-) delete mode 100644 crates/lib/cfsctl/Cargo.toml rename crates/lib/{cfsctl/src/main.rs => src/cfsctl.rs} (86%) diff --git a/Cargo.lock b/Cargo.lock index 871d12877..158cf1654 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,6 +241,9 @@ dependencies = [ "clap", "clap_mangen", "comfy-table", + "composefs", + "composefs-boot", + "composefs-oci", "fn-error-context", "hex", "indicatif", diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index e87a2a87a..14b586d5d 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -23,6 +23,9 @@ bootc-mount = { path = "../mount" } bootc-tmpfiles = { path = "../tmpfiles" } bootc-sysusers = { path = "../sysusers" } camino = { workspace = true, features = ["serde1"] } +composefs = { workspace = true } +composefs-boot = { workspace = true } +composefs-oci = { workspace = true } ostree-ext = { path = "../ostree-ext", features = ["bootc"] } chrono = { workspace = true, features = ["serde"] } clap = { workspace = true, features = ["derive","cargo"] } diff --git a/crates/lib/cfsctl/Cargo.toml b/crates/lib/cfsctl/Cargo.toml deleted file mode 100644 index 4901f490f..000000000 --- a/crates/lib/cfsctl/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "cfsctl" -description = "Command-line utility for composefs" -default-run = "cfsctl" - -edition.workspace = true -license.workspace = true -readme.workspace = true -repository.workspace = true -rust-version.workspace = true -version.workspace = true - -[features] -default = ['pre-6.15', 'oci'] -http = ['composefs-http'] -oci = ['composefs-oci'] -rhel9 = ['composefs/rhel9'] -'pre-6.15' = ['composefs/pre-6.15'] - -[dependencies] -anyhow = { version = "1.0.87", default-features = false } -clap = { version = "4.0.1", default-features = false, features = ["std", "help", "usage", "derive"] } -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"] } -tokio = { version = "1.24.2", default-features = false } - -[lints] -workspace = true diff --git a/crates/lib/cfsctl/src/main.rs b/crates/lib/src/cfsctl.rs similarity index 86% rename from crates/lib/cfsctl/src/main.rs rename to crates/lib/src/cfsctl.rs index dd66a949f..1c5642535 100644 --- a/crates/lib/cfsctl/src/main.rs +++ b/crates/lib/src/cfsctl.rs @@ -1,4 +1,5 @@ use std::{ + ffi::OsString, fs::create_dir_all, path::{Path, PathBuf}, sync::Arc, @@ -12,7 +13,7 @@ use rustix::fs::CWD; use composefs_boot::{write_boot, BootOps}; use composefs::{ - fsverity::{FsVerityHashValue, Sha256HashValue}, + fsverity::{FsVerityHashValue, Sha512HashValue}, repository::Repository, }; @@ -37,7 +38,6 @@ pub struct App { cmd: Command, } -#[cfg(feature = "oci")] #[derive(Debug, Subcommand)] enum OciCommand { /// Stores a tar file as a splitstream in the repository. @@ -109,7 +109,6 @@ enum Command { reference: String, }, /// Commands for dealing with OCI layers - #[cfg(feature = "oci")] Oci { #[clap(subcommand)] cmd: OciCommand, @@ -146,39 +145,39 @@ enum Command { ImageObjects { name: String, }, - #[cfg(feature = "http")] - Fetch { - url: String, - name: String, - }, } -fn verity_opt(opt: &Option) -> Result> { - Ok(match opt { - Some(value) => Some(FsVerityHashValue::from_hex(value)?), - None => None, - }) +fn verity_opt(opt: &Option) -> Result> { + Ok(opt + .as_ref() + .map(|value| FsVerityHashValue::from_hex(value)) + .transpose()?) } -#[tokio::main] -async fn main() -> Result<()> { - env_logger::init(); +pub(crate) async fn run_from_iter(system_store: &crate::store::Storage, args: I) -> Result<()> +where + I: IntoIterator, + I::Item: Into + Clone, +{ + let args = App::parse_from( + std::iter::once(OsString::from("cfs")).chain(args.into_iter().map(Into::into)), + ); - let args = App::parse(); - - let mut repo: Repository = (if let Some(path) = &args.repo { - Repository::open_path(CWD, path) - } else if args.system { - Repository::open_system() + let repo = if let Some(path) = &args.repo { + let mut r = Repository::open_path(CWD, path)?; + r.set_insecure(args.insecure); + Arc::new(r) } else if args.user { - Repository::open_user() - } else if rustix::process::getuid().is_root() { - Repository::open_system() + let mut r = Repository::open_user()?; + r.set_insecure(args.insecure); + Arc::new(r) } else { - Repository::open_user() - })?; - - repo.set_insecure(args.insecure); + if args.insecure { + anyhow::bail!("Cannot override insecure state for system repo"); + } + system_store.get_ensure_composefs()? + }; + let repo = &repo; match args.cmd { Command::Transaction => { @@ -194,11 +193,10 @@ async fn main() -> Result<()> { let image_id = repo.import_image(&reference, &mut std::io::stdin())?; println!("{}", image_id.to_id()); } - #[cfg(feature = "oci")] Command::Oci { cmd: oci_cmd } => match oci_cmd { OciCommand::ImportLayer { name, sha256 } => { let object_id = composefs_oci::import_layer( - &Arc::new(repo), + &repo, &composefs::util::parse_sha256(sha256)?, name.as_deref(), &mut std::io::stdin(), @@ -247,8 +245,7 @@ async fn main() -> Result<()> { println!("{}", image_id.to_id()); } OciCommand::Pull { ref image, name } => { - let (sha256, verity) = - composefs_oci::pull(&Arc::new(repo), image, name.as_deref()).await?; + let (sha256, verity) = composefs_oci::pull(&repo, image, name.as_deref()).await?; println!("sha256 {}", hex::encode(sha256)); println!("verity {}", verity.to_hex()); @@ -258,8 +255,7 @@ async fn main() -> Result<()> { ref config_verity, } => { let verity = verity_opt(config_verity)?; - let (sha256, verity) = - composefs_oci::seal(&Arc::new(repo), config_name, verity.as_ref())?; + let (sha256, verity) = composefs_oci::seal(&repo, config_name, verity.as_ref())?; println!("sha256 {}", hex::encode(sha256)); println!("verity {}", verity.to_id()); } @@ -301,7 +297,7 @@ async fn main() -> Result<()> { let state = args .repo .as_ref() - .map(|p: &PathBuf| p.parent().unwrap()) + .map(|p: &PathBuf| p.parent().unwrap_or(p)) .unwrap_or(Path::new("/sysroot")) .join("state/deploy") .join(id.to_hex()); @@ -359,12 +355,6 @@ async fn main() -> Result<()> { Command::GC => { repo.gc()?; } - #[cfg(feature = "http")] - Command::Fetch { url, name } => { - let (sha256, verity) = composefs_http::download(&url, &name, Arc::new(repo)).await?; - println!("sha256 {}", hex::encode(sha256)); - println!("verity {}", verity.to_hex()); - } } Ok(()) } diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index e2f298a09..4aa032e55 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -458,6 +458,11 @@ pub(crate) enum InternalsOpts { #[clap(allow_hyphen_values = true)] args: Vec, }, + /// Proxy frontend for the `cfsctl` CLI + Cfs { + #[clap(allow_hyphen_values = true)] + args: Vec, + }, /// Proxy frontend for the legacy `ostree container` CLI. OstreeContainer { #[clap(allow_hyphen_values = true)] @@ -1259,6 +1264,10 @@ async fn run_from_opt(opt: Opt) -> Result<()> { Ok(()) } }, + InternalsOpts::Cfs { args } => { + let sysroot = &get_storage().await?; + crate::cfsctl::run_from_iter(sysroot, args.iter()).await + } InternalsOpts::Reboot => crate::reboot::reboot(), InternalsOpts::Fsck => { let sysroot = &get_storage().await?; diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index b850519e0..377a2191a 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -5,6 +5,7 @@ //! bootable container images. mod boundimage; +mod cfsctl; pub mod cli; pub(crate) mod deploy; pub(crate) mod fsck; diff --git a/tmt/tests/booted/readonly/030-test-composefs.nu b/tmt/tests/booted/readonly/030-test-composefs.nu index 1c185bfb6..31e149e78 100644 --- a/tmt/tests/booted/readonly/030-test-composefs.nu +++ b/tmt/tests/booted/readonly/030-test-composefs.nu @@ -5,4 +5,8 @@ tap begin "composefs integration smoke test" bootc internals test-composefs +bootc internals cfs --help +bootc internals cfs oci pull docker://busybox busybox +test -L /sysroot/composefs/streams/refs/busybox + tap ok