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/src/cfsctl.rs b/crates/lib/src/cfsctl.rs new file mode 100644 index 000000000..1c5642535 --- /dev/null +++ b/crates/lib/src/cfsctl.rs @@ -0,0 +1,360 @@ +use std::{ + ffi::OsString, + 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, Sha512HashValue}, + 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, + + /// 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, +} + +#[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 + 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(opt + .as_ref() + .map(|value| FsVerityHashValue::from_hex(value)) + .transpose()?) +} + +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 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 { + let mut r = Repository::open_user()?; + r.set_insecure(args.insecure); + Arc::new(r) + } else { + 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 => { + // 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()); + } + Command::Oci { cmd: oci_cmd } => match oci_cmd { + OciCommand::ImportLayer { name, sha256 } => { + let object_id = composefs_oci::import_layer( + &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(&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(&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, + args.insecure, + bootdir, + None, + entry_id.as_deref(), + &cmdline_refs, + )?; + + let state = args + .repo + .as_ref() + .map(|p: &PathBuf| p.parent().unwrap_or(p)) + .unwrap_or(Path::new("/sysroot")) + .join("state/deploy") + .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_at(&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(()) +} 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