diff --git a/cli/Cargo.toml b/cli/Cargo.toml index ec3c22f5..23cbed59 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -11,7 +11,6 @@ readme = "README.md" anyhow = "1.0" ostree-ext = { path = "../lib" } clap = "2.33.3" -indicatif = "0.15.0" structopt = "0.3.21" ostree = { version = "0.11.0", features = ["v2021_2"] } libc = "0.2.92" diff --git a/cli/src/main.rs b/cli/src/main.rs index 2387ba03..64910451 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,204 +1,9 @@ use anyhow::Result; -use std::convert::TryInto; -use structopt::StructOpt; - -#[derive(Debug, StructOpt)] -struct BuildOpts { - #[structopt(long)] - repo: String, - - #[structopt(long = "ref")] - ostree_ref: String, - - #[structopt(long)] - oci_dir: String, -} - -#[derive(Debug, StructOpt)] -struct ImportOpts { - /// Path to the repository - #[structopt(long)] - repo: String, - - /// Path to a tar archive; if unspecified, will be stdin. Currently the tar archive must not be compressed. - path: Option, -} - -#[derive(Debug, StructOpt)] -struct ExportOpts { - /// Path to the repository - #[structopt(long)] - repo: String, - - /// The ostree ref or commit to export - rev: String, -} - -#[derive(Debug, StructOpt)] -enum TarOpts { - /// Import a tar archive (currently, must not be compressed) - Import(ImportOpts), - - /// Write a tar archive to stdout - Export(ExportOpts), -} - -#[derive(Debug, StructOpt)] -enum ContainerOpts { - /// Import an ostree commit embedded in a remote container image - Import { - /// Path to the repository - #[structopt(long)] - repo: String, - - /// Image reference, e.g. registry:quay.io/exampleos/exampleos:latest - imgref: String, - }, - - /// Print information about an exported ostree-container image. - Info { - /// Image reference, e.g. registry:quay.io/exampleos/exampleos:latest - imgref: String, - }, - - /// Export an ostree commit to an OCI layout - Export { - /// Path to the repository - #[structopt(long)] - repo: String, - - /// The ostree ref or commit to export - rev: String, - - /// Image reference, e.g. registry:quay.io/exampleos/exampleos:latest - imgref: String, - }, -} - -#[derive(Debug, StructOpt)] -struct ImaSignOpts { - /// Path to the repository - #[structopt(long)] - repo: String, - /// The ostree ref or commit to use as a base - src_rev: String, - /// The ostree ref to use for writing the signed commit - target_ref: String, - - /// Digest algorithm - algorithm: String, - /// Path to IMA key - key: String, -} - -#[derive(Debug, StructOpt)] -#[structopt(name = "ostree-ext")] -#[structopt(rename_all = "kebab-case")] -enum Opt { - /// Import and export to tar - Tar(TarOpts), - /// Import and export to a container image - Container(ContainerOpts), - ImaSign(ImaSignOpts), -} - -async fn tar_import(opts: &ImportOpts) -> Result<()> { - let repo = &ostree::Repo::open_at(libc::AT_FDCWD, opts.repo.as_str(), gio::NONE_CANCELLABLE)?; - let imported = if let Some(path) = opts.path.as_ref() { - let instream = tokio::fs::File::open(path).await?; - ostree_ext::tar::import_tar(repo, instream).await? - } else { - let stdin = tokio::io::stdin(); - ostree_ext::tar::import_tar(repo, stdin).await? - }; - println!("Imported: {}", imported); - Ok(()) -} - -fn tar_export(opts: &ExportOpts) -> Result<()> { - let repo = &ostree::Repo::open_at(libc::AT_FDCWD, opts.repo.as_str(), gio::NONE_CANCELLABLE)?; - ostree_ext::tar::export_commit(repo, opts.rev.as_str(), std::io::stdout())?; - Ok(()) -} - -async fn container_import(repo: &str, imgref: &str) -> Result<()> { - let repo = &ostree::Repo::open_at(libc::AT_FDCWD, repo, gio::NONE_CANCELLABLE)?; - let imgref = imgref.try_into()?; - let (tx_progress, rx_progress) = tokio::sync::watch::channel(Default::default()); - let target = indicatif::ProgressDrawTarget::stdout(); - let style = indicatif::ProgressStyle::default_bar(); - let pb = indicatif::ProgressBar::new_spinner(); - pb.set_draw_target(target); - pb.set_style(style.template("{spinner} {prefix} {msg}")); - pb.enable_steady_tick(200); - pb.set_message("Downloading..."); - let import = ostree_ext::container::import(repo, &imgref, Some(tx_progress)); - tokio::pin!(import); - tokio::pin!(rx_progress); - loop { - tokio::select! { - _ = rx_progress.changed() => { - let n = rx_progress.borrow().processed_bytes; - pb.set_message(&format!("Processed: {}", indicatif::HumanBytes(n))); - } - import = &mut import => { - pb.finish(); - println!("Imported: {}", import?.ostree_commit); - return Ok(()) - } - } - } -} - -async fn container_export(repo: &str, rev: &str, imgref: &str) -> Result<()> { - let repo = &ostree::Repo::open_at(libc::AT_FDCWD, repo, gio::NONE_CANCELLABLE)?; - let imgref = imgref.try_into()?; - let pushed = ostree_ext::container::export(repo, rev, &imgref).await?; - println!("{}", pushed); - Ok(()) -} - -async fn container_info(imgref: &str) -> Result<()> { - let imgref = imgref.try_into()?; - let info = ostree_ext::container::fetch_manifest_info(&imgref).await?; - println!("{} @{}", imgref, info.manifest_digest); - Ok(()) -} - -fn ima_sign(cmdopts: &ImaSignOpts) -> Result<()> { - let repo = - &ostree::Repo::open_at(libc::AT_FDCWD, cmdopts.repo.as_str(), gio::NONE_CANCELLABLE)?; - let signopts = ostree_ext::ima::ImaOpts { - algorithm: cmdopts.algorithm.clone(), - key: cmdopts.key.clone(), - }; - let signed_commit = ostree_ext::ima::ima_sign(repo, cmdopts.src_rev.as_str(), &signopts)?; - repo.set_ref_immediate( - None, - cmdopts.target_ref.as_str(), - Some(signed_commit.as_str()), - gio::NONE_CANCELLABLE, - )?; - println!("{} => {}", cmdopts.target_ref, signed_commit); - Ok(()) -} async fn run() -> Result<()> { tracing_subscriber::fmt::init(); tracing::trace!("starting"); - let opt = Opt::from_args(); - match opt { - Opt::Tar(TarOpts::Import(ref opt)) => tar_import(opt).await, - Opt::Tar(TarOpts::Export(ref opt)) => tar_export(opt), - Opt::Container(ContainerOpts::Info { imgref }) => container_info(imgref.as_str()).await, - Opt::Container(ContainerOpts::Import { repo, imgref }) => { - container_import(&repo, &imgref).await - } - Opt::Container(ContainerOpts::Export { repo, rev, imgref }) => { - container_export(&repo, &rev, &imgref).await - } - Opt::ImaSign(ref opts) => ima_sign(opts), - } + ostree_ext::cli::run_from_iter(std::env::args_os()).await } #[tokio::main] diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 3f3b1ea8..5a7f8d71 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -16,6 +16,7 @@ cjson = "0.1.1" flate2 = { features = ["zlib"], default_features = false, version = "1.0.20" } fn-error-context = "0.1.1" futures = "0.3.13" +indicatif = "0.15.0" gio = "0.9.1" glib = "0.10.3" glib-sys = "0.10.1" @@ -31,6 +32,7 @@ ostree = { features = ["v2021_2"], version = "0.11.0" } ostree-sys = "0.7.2" serde = { features = ["derive"], version = "1.0.125" } serde_json = "1.0.64" +structopt = "0.3.21" tar = "0.4.33" tempfile = "3.2.0" tokio = { features = ["full"], version = "1" } diff --git a/lib/src/cli.rs b/lib/src/cli.rs new file mode 100644 index 00000000..f38b9f34 --- /dev/null +++ b/lib/src/cli.rs @@ -0,0 +1,227 @@ +//! # Commandline parsing +//! +//! While there is a separate `ostree-ext-cli` crate that +//! can be installed and used directly, the CLI code is +//! also exported as a library too, so that projects +//! such as `rpm-ostree` can directly reuse it. + +use anyhow::Result; +use std::convert::TryInto; +use std::ffi::OsString; +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +struct BuildOpts { + #[structopt(long)] + repo: String, + + #[structopt(long = "ref")] + ostree_ref: String, + + #[structopt(long)] + oci_dir: String, +} + +/// Options for importing a tar archive. +#[derive(Debug, StructOpt)] +struct ImportOpts { + /// Path to the repository + #[structopt(long)] + repo: String, + + /// Path to a tar archive; if unspecified, will be stdin. Currently the tar archive must not be compressed. + path: Option, +} + +/// Options for exporting a tar archive. +#[derive(Debug, StructOpt)] +struct ExportOpts { + /// Path to the repository + #[structopt(long)] + repo: String, + + /// The ostree ref or commit to export + rev: String, +} + +/// Options for import/export to tar archives. +#[derive(Debug, StructOpt)] +enum TarOpts { + /// Import a tar archive (currently, must not be compressed) + Import(ImportOpts), + + /// Write a tar archive to stdout + Export(ExportOpts), +} + +/// Options for container import/export. +#[derive(Debug, StructOpt)] +enum ContainerOpts { + /// Import an ostree commit embedded in a remote container image + Import { + /// Path to the repository + #[structopt(long)] + repo: String, + + /// Image reference, e.g. registry:quay.io/exampleos/exampleos:latest + imgref: String, + }, + + /// Print information about an exported ostree-container image. + Info { + /// Image reference, e.g. registry:quay.io/exampleos/exampleos:latest + imgref: String, + }, + + /// Export an ostree commit to an OCI layout + Export { + /// Path to the repository + #[structopt(long)] + repo: String, + + /// The ostree ref or commit to export + rev: String, + + /// Image reference, e.g. registry:quay.io/exampleos/exampleos:latest + imgref: String, + }, +} + +/// Options for the Integrity Measurement Architecture (IMA). +#[derive(Debug, StructOpt)] +struct ImaSignOpts { + /// Path to the repository + #[structopt(long)] + repo: String, + /// The ostree ref or commit to use as a base + src_rev: String, + /// The ostree ref to use for writing the signed commit + target_ref: String, + + /// Digest algorithm + algorithm: String, + /// Path to IMA key + key: String, +} + +/// Toplevel options for extended ostree functionality. +#[derive(Debug, StructOpt)] +#[structopt(name = "ostree-ext")] +#[structopt(rename_all = "kebab-case")] +enum Opt { + /// Import and export to tar + Tar(TarOpts), + /// Import and export to a container image + Container(ContainerOpts), + /// IMA signatures + ImaSign(ImaSignOpts), +} + +/// Import a tar archive containing an ostree commit. +async fn tar_import(opts: &ImportOpts) -> Result<()> { + let repo = &ostree::Repo::open_at(libc::AT_FDCWD, opts.repo.as_str(), gio::NONE_CANCELLABLE)?; + let imported = if let Some(path) = opts.path.as_ref() { + let instream = tokio::fs::File::open(path).await?; + crate::tar::import_tar(repo, instream).await? + } else { + let stdin = tokio::io::stdin(); + crate::tar::import_tar(repo, stdin).await? + }; + println!("Imported: {}", imported); + Ok(()) +} + +/// Export a tar archive containing an ostree commit. +fn tar_export(opts: &ExportOpts) -> Result<()> { + let repo = &ostree::Repo::open_at(libc::AT_FDCWD, opts.repo.as_str(), gio::NONE_CANCELLABLE)?; + crate::tar::export_commit(repo, opts.rev.as_str(), std::io::stdout())?; + Ok(()) +} + +/// Import a container image with an encapsulated ostree commit. +async fn container_import(repo: &str, imgref: &str) -> Result<()> { + let repo = &ostree::Repo::open_at(libc::AT_FDCWD, repo, gio::NONE_CANCELLABLE)?; + let imgref = imgref.try_into()?; + let (tx_progress, rx_progress) = tokio::sync::watch::channel(Default::default()); + let target = indicatif::ProgressDrawTarget::stdout(); + let style = indicatif::ProgressStyle::default_bar(); + let pb = indicatif::ProgressBar::new_spinner(); + pb.set_draw_target(target); + pb.set_style(style.template("{spinner} {prefix} {msg}")); + pb.enable_steady_tick(200); + pb.set_message("Downloading..."); + let import = crate::container::import(repo, &imgref, Some(tx_progress)); + tokio::pin!(import); + tokio::pin!(rx_progress); + loop { + tokio::select! { + _ = rx_progress.changed() => { + let n = rx_progress.borrow().processed_bytes; + pb.set_message(&format!("Processed: {}", indicatif::HumanBytes(n))); + } + import = &mut import => { + pb.finish(); + println!("Imported: {}", import?.ostree_commit); + return Ok(()) + } + } + } +} + +/// Export a container image with an encapsulated ostree commit. +async fn container_export(repo: &str, rev: &str, imgref: &str) -> Result<()> { + let repo = &ostree::Repo::open_at(libc::AT_FDCWD, repo, gio::NONE_CANCELLABLE)?; + let imgref = imgref.try_into()?; + let pushed = crate::container::export(repo, rev, &imgref).await?; + println!("{}", pushed); + Ok(()) +} + +/// Load metadata for a container image with an encapsulated ostree commit. +async fn container_info(imgref: &str) -> Result<()> { + let imgref = imgref.try_into()?; + let info = crate::container::fetch_manifest_info(&imgref).await?; + println!("{} @{}", imgref, info.manifest_digest); + Ok(()) +} + +/// Add IMA signatures to an ostree commit, generating a new commit. +fn ima_sign(cmdopts: &ImaSignOpts) -> Result<()> { + let repo = + &ostree::Repo::open_at(libc::AT_FDCWD, cmdopts.repo.as_str(), gio::NONE_CANCELLABLE)?; + let signopts = crate::ima::ImaOpts { + algorithm: cmdopts.algorithm.clone(), + key: cmdopts.key.clone(), + }; + let signed_commit = crate::ima::ima_sign(repo, cmdopts.src_rev.as_str(), &signopts)?; + repo.set_ref_immediate( + None, + cmdopts.target_ref.as_str(), + Some(signed_commit.as_str()), + gio::NONE_CANCELLABLE, + )?; + println!("{} => {}", cmdopts.target_ref, signed_commit); + Ok(()) +} + +/// Parse the provided arguments and execute. +/// Calls [`clap::Error::exit`] on failure, printing the error message and aborting the program. +pub async fn run_from_iter(args: I) -> Result<()> +where + I: IntoIterator, + I::Item: Into + Clone, +{ + let opt = Opt::from_iter(args); + match opt { + Opt::Tar(TarOpts::Import(ref opt)) => tar_import(opt).await, + Opt::Tar(TarOpts::Export(ref opt)) => tar_export(opt), + Opt::Container(ContainerOpts::Info { imgref }) => container_info(imgref.as_str()).await, + Opt::Container(ContainerOpts::Import { repo, imgref }) => { + container_import(&repo, &imgref).await + } + Opt::Container(ContainerOpts::Export { repo, rev, imgref }) => { + container_export(&repo, &rev, &imgref).await + } + Opt::ImaSign(ref opts) => ima_sign(opts), + } +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index c7a284f5..2c097db1 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -14,6 +14,7 @@ type Result = anyhow::Result; mod async_util; +pub mod cli; pub mod container; pub mod diff; pub mod ima;