From e6cce3d54294085d8ebf1233a0b48a8d0f07f820 Mon Sep 17 00:00:00 2001 From: Gerald Pinder Date: Wed, 4 Sep 2024 18:17:08 -0400 Subject: [PATCH] feat(iso): Create generate-iso command (#192) ## Tasks - [x] Add ctrl-c handler to kill spawned children - [x] add more args to support all variables - [x] Add integration test --- .github/workflows/build-pr.yml | 94 ++++++++++++ .github/workflows/build.yml | 94 ++++++++++++ Cargo.lock | 57 +++++++- Cargo.toml | 4 + process/Cargo.toml | 2 +- process/drivers.rs | 52 +++---- process/drivers/docker_driver.rs | 7 +- process/drivers/github_driver.rs | 47 +++--- process/drivers/gitlab_driver.rs | 44 +++--- process/drivers/local_driver.rs | 35 +++-- process/drivers/opts.rs | 2 + process/drivers/opts/build.rs | 2 + process/drivers/opts/ci.rs | 12 ++ process/drivers/opts/run.rs | 8 +- process/drivers/podman_driver.rs | 20 ++- process/drivers/traits.rs | 32 +++-- process/process.rs | 37 ----- recipe/Cargo.toml | 1 + recipe/src/recipe.rs | 22 ++- recipe/src/stage.rs | 2 +- src/bin/bluebuild.rs | 3 + src/commands.rs | 6 + src/commands/build.rs | 101 ++++++------- src/commands/generate.rs | 16 +-- src/commands/generate_iso.rs | 240 +++++++++++++++++++++++++++++++ 25 files changed, 738 insertions(+), 202 deletions(-) create mode 100644 process/drivers/opts/ci.rs create mode 100644 src/commands/generate_iso.rs diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 26207dd8..98e0b8f8 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -416,3 +416,97 @@ jobs: bluebuild template -vv | tee Containerfile grep -q 'ARG IMAGE_REGISTRY=ghcr.io/blue-build' Containerfile || exit 1 bluebuild build --retry-push -B buildah -I podman -S sigstore --squash --push -vv recipes/recipe.yml recipes/recipe-39.yml + + iso-from-image: + timeout-minutes: 60 + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + needs: + - build + if: needs.build.outputs.push == 'true' + + steps: + - name: Maximize build space + uses: ublue-os/remove-unwanted-software@v6 + + - uses: sigstore/cosign-installer@v3.3.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + install: true + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + + - name: Install bluebuild + run: | + cargo install --path . --debug --all-features + + - name: Expose GitHub Runtime + uses: crazy-max/ghaction-github-runtime@v3 + + - name: Run Build + env: + GH_TOKEN: ${{ github.token }} + GH_PR_EVENT_NUMBER: ${{ github.event.number }} + COSIGN_PRIVATE_KEY: ${{ secrets.TEST_SIGNING_SECRET }} + BB_BUILDKIT_CACHE_GHA: true + run: | + cd integration-tests/test-repo + bluebuild generate-iso image ghcr.io/blue-build/cli/test:40 + + iso-from-recipe: + timeout-minutes: 60 + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + needs: + - build + if: needs.build.outputs.push == 'true' + + steps: + - name: Maximize build space + uses: ublue-os/remove-unwanted-software@v6 + + - uses: sigstore/cosign-installer@v3.3.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + install: true + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + + - name: Install bluebuild + run: | + cargo install --path . --debug --all-features + + - name: Expose GitHub Runtime + uses: crazy-max/ghaction-github-runtime@v3 + + - name: Run Build + env: + GH_TOKEN: ${{ github.token }} + GH_PR_EVENT_NUMBER: ${{ github.event.number }} + COSIGN_PRIVATE_KEY: ${{ secrets.TEST_SIGNING_SECRET }} + BB_BUILDKIT_CACHE_GHA: true + run: | + cd integration-tests/test-repo + bluebuild generate-iso -vv recipe recipes/recipe.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 00b43193..d1186b86 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -414,3 +414,97 @@ jobs: bluebuild template -vv | tee Containerfile grep -q 'ARG IMAGE_REGISTRY=ghcr.io/blue-build' Containerfile || exit 1 bluebuild build --retry-push -B buildah -I podman -S sigstore --squash --push -vv recipes/recipe.yml recipes/recipe-39.yml + + iso-from-image: + timeout-minutes: 60 + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + needs: + - build + if: github.repository == 'blue-build/cli' + + steps: + - name: Maximize build space + uses: ublue-os/remove-unwanted-software@v6 + + - uses: sigstore/cosign-installer@v3.3.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + install: true + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + + - name: Install bluebuild + run: | + cargo install --path . --debug --all-features + + - name: Expose GitHub Runtime + uses: crazy-max/ghaction-github-runtime@v3 + + - name: Run Build + env: + GH_TOKEN: ${{ github.token }} + GH_PR_EVENT_NUMBER: ${{ github.event.number }} + COSIGN_PRIVATE_KEY: ${{ secrets.TEST_SIGNING_SECRET }} + BB_BUILDKIT_CACHE_GHA: true + run: | + cd integration-tests/test-repo + bluebuild generate-iso image ghcr.io/blue-build/cli/test:40 + + iso-from-recipe: + timeout-minutes: 60 + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + needs: + - build + if: github.repository == 'blue-build/cli' + + steps: + - name: Maximize build space + uses: ublue-os/remove-unwanted-software@v6 + + - uses: sigstore/cosign-installer@v3.3.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + install: true + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + + - name: Install bluebuild + run: | + cargo install --path . --debug --all-features + + - name: Expose GitHub Runtime + uses: crazy-max/ghaction-github-runtime@v3 + + - name: Run Build + env: + GH_TOKEN: ${{ github.token }} + GH_PR_EVENT_NUMBER: ${{ github.event.number }} + COSIGN_PRIVATE_KEY: ${{ secrets.TEST_SIGNING_SECRET }} + BB_BUILDKIT_CACHE_GHA: true + run: | + cd integration-tests/test-repo + bluebuild generate-iso -vv recipe recipes/recipe.yml diff --git a/Cargo.lock b/Cargo.lock index b92ab6ec..2455be89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -328,6 +328,7 @@ dependencies = [ "indicatif", "log", "miette", + "oci-distribution", "open", "os_info", "rayon", @@ -348,7 +349,6 @@ name = "blue-build-process-management" version = "0.8.14" dependencies = [ "anyhow", - "blue-build-recipe", "blue-build-utils", "chrono", "clap", @@ -363,6 +363,7 @@ dependencies = [ "miette", "nix", "nu-ansi-term", + "oci-distribution", "once_cell", "os_pipe", "rand 0.8.5", @@ -389,6 +390,7 @@ dependencies = [ "indexmap 2.3.0", "log", "miette", + "oci-distribution", "serde", "serde_json", "serde_yaml 0.9.34+deprecated", @@ -1626,6 +1628,7 @@ dependencies = [ "hyper 1.4.1", "hyper-util", "rustls 0.23.12", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", @@ -2554,6 +2557,12 @@ dependencies = [ "url", ] +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + [[package]] name = "option-ext" version = "0.2.0" @@ -3320,6 +3329,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls 0.23.12", + "rustls-native-certs", "rustls-pemfile 2.1.3", "rustls-pki-types", "serde", @@ -3514,6 +3524,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88d6d420651b496bdd98684116959239430022a115c1240e6c3993be0b15fba" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.1.3", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -3608,6 +3631,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -3650,6 +3682,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index 028d837c..62cee955 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ colored = "2" indexmap = { version = "2", features = ["serde"] } indicatif = { version = "0.17", features = ["improved_unicode"] } log = "0.4" +oci-distribution = { version = "0.11.0", default-features = false, features = ["rustls-tls", "rustls-tls-native-roots"] } miette = "7" rstest = "0.18" serde = { version = "1", features = ["derive"] } @@ -37,6 +38,7 @@ style = "deny" nursery = "deny" pedantic = "deny" module_name_repetitions = { level = "allow", priority = 1 } +doc_markdown = { level = "allow", priority = 1 } [package] name = "blue-build" @@ -74,6 +76,7 @@ colored.workspace = true indicatif.workspace = true log.workspace = true miette = { workspace = true, features = ["fancy"] } +oci-distribution.workspace = true serde.workspace = true serde_json.workspace = true serde_yaml.workspace = true @@ -86,6 +89,7 @@ default = [] stages = ["blue-build-recipe/stages"] copy = ["blue-build-recipe/copy"] multi-recipe = ["rayon", "indicatif/rayon"] +iso = [] switch = [] sigstore = ["blue-build-process-management/sigstore"] login = [] diff --git a/process/Cargo.toml b/process/Cargo.toml index 7fa65e80..0d202ff2 100644 --- a/process/Cargo.toml +++ b/process/Cargo.toml @@ -12,7 +12,6 @@ path = "process.rs" [dependencies] anyhow = "1" -blue-build-recipe = { version = "=0.8.14", path = "../recipe" } blue-build-utils = { version = "=0.8.14", path = "../utils" } expect-exit = "0.5" indicatif-log-bridge = "0.2" @@ -36,6 +35,7 @@ indicatif.workspace = true indexmap.workspace = true log.workspace = true miette.workspace = true +oci-distribution.workspace = true serde.workspace = true serde_json.workspace = true tempdir.workspace = true diff --git a/process/drivers.rs b/process/drivers.rs index e0ab4b82..596fb6b8 100644 --- a/process/drivers.rs +++ b/process/drivers.rs @@ -11,12 +11,13 @@ use std::{ sync::{Mutex, RwLock}, }; -use blue_build_recipe::Recipe; use blue_build_utils::constants::IMAGE_VERSION_LABEL; use clap::Args; use log::{debug, info, trace}; use miette::{miette, Result}; +use oci_distribution::Reference; use once_cell::sync::Lazy; +use opts::GenerateTagsOpts; #[cfg(feature = "sigstore")] use sigstore_driver::SigstoreDriver; use typed_builder::TypedBuilder; @@ -193,33 +194,31 @@ impl Driver { /// /// # Panics /// Panics if the mutex fails to lock. - pub fn get_os_version(recipe: &Recipe) -> Result { + pub fn get_os_version(oci_ref: &Reference) -> Result { #[cfg(test)] { - use miette::IntoDiagnostic; - let _ = recipe; // silence lint + let _ = oci_ref; // silence lint if true { - return crate::test::create_test_recipe() - .image_version - .parse() - .into_diagnostic(); + return Ok(40); } } - trace!("Driver::get_os_version({recipe:#?})"); - let image = format!("{}:{}", &recipe.base_image, &recipe.image_version); - + trace!("Driver::get_os_version({oci_ref:#?})"); let mut os_version_lock = OS_VERSION.lock().expect("Should lock"); - let entry = os_version_lock.get(&image); + let entry = os_version_lock.get(&oci_ref.to_string()); let os_version = match entry { None => { - info!("Retrieving OS version from {image}. This might take a bit"); + info!("Retrieving OS version from {oci_ref}. This might take a bit"); let inspect_opts = GetMetadataOpts::builder() - .image(&*recipe.base_image) - .tag(&*recipe.image_version) + .image(format!( + "{}/{}", + oci_ref.resolve_registry(), + oci_ref.repository() + )) + .tag(oci_ref.tag().unwrap_or("latest")) .build(); let inspection = Self::get_metadata(&inspect_opts)?; @@ -234,13 +233,13 @@ impl Driver { os_version } Some(os_version) => { - debug!("Found cached {os_version} for {image}"); + debug!("Found cached {os_version} for {oci_ref}"); *os_version } }; - if let Entry::Vacant(entry) = os_version_lock.entry(image.clone()) { - trace!("Caching version {os_version} for {image}"); + if let Entry::Vacant(entry) = os_version_lock.entry(oci_ref.to_string()) { + trace!("Caching version {os_version} for {oci_ref}"); entry.insert(os_version); } drop(os_version_lock); @@ -371,9 +370,9 @@ impl RunDriver for Driver { macro_rules! impl_ci_driver { ($func:ident($($args:expr),*)) => { match Self::get_ci_driver() { - CiDriverType::Local => LocalDriver::$func($($args)*), - CiDriverType::Gitlab => GitlabDriver::$func($($args)*), - CiDriverType::Github => GithubDriver::$func($($args)*), + CiDriverType::Local => LocalDriver::$func($($args,)*), + CiDriverType::Gitlab => GitlabDriver::$func($($args,)*), + CiDriverType::Github => GithubDriver::$func($($args,)*), } }; } @@ -391,8 +390,8 @@ impl CiDriver for Driver { impl_ci_driver!(oidc_provider()) } - fn generate_tags(recipe: &Recipe) -> Result> { - impl_ci_driver!(generate_tags(recipe)) + fn generate_tags(opts: &GenerateTagsOpts) -> Result> { + impl_ci_driver!(generate_tags(opts)) } fn get_repo_url() -> Result { @@ -403,7 +402,10 @@ impl CiDriver for Driver { impl_ci_driver!(get_registry()) } - fn generate_image_name(recipe: &Recipe) -> Result { - impl_ci_driver!(generate_image_name(recipe)) + fn generate_image_name(name: S) -> Result + where + S: AsRef, + { + impl_ci_driver!(generate_image_name(name)) } } diff --git a/process/drivers/docker_driver.rs b/process/drivers/docker_driver.rs index 9bfe9877..a05922e0 100644 --- a/process/drivers/docker_driver.rs +++ b/process/drivers/docker_driver.rs @@ -373,7 +373,7 @@ impl RunDriver for DockerDriver { } fn docker_run(opts: &RunOpts, cid_file: &Path) -> Command { - cmd!( + let command = cmd!( "docker", "run", format!("--cidfile={}", cid_file.display()), @@ -397,5 +397,8 @@ fn docker_run(opts: &RunOpts, cid_file: &Path) -> Command { }, &*opts.image, for opts.args, - ) + ); + trace!("{command:?}"); + + command } diff --git a/process/drivers/github_driver.rs b/process/drivers/github_driver.rs index f2d16d5b..75d010d4 100644 --- a/process/drivers/github_driver.rs +++ b/process/drivers/github_driver.rs @@ -14,7 +14,7 @@ use blue_build_utils::get_env_var; #[cfg(test)] use blue_build_utils::test_utils::get_env_var; -use super::{CiDriver, Driver}; +use super::{opts::GenerateTagsOpts, CiDriver, Driver}; mod event; @@ -34,10 +34,11 @@ impl CiDriver for GithubDriver { Ok(GITHUB_TOKEN_ISSUER_URL.to_string()) } - fn generate_tags(recipe: &blue_build_recipe::Recipe) -> miette::Result> { + fn generate_tags(opts: &GenerateTagsOpts) -> miette::Result> { const PR_EVENT: &str = "pull_request"; let timestamp = blue_build_utils::get_tag_timestamp(); - let os_version = Driver::get_os_version(recipe).inspect(|v| trace!("os_version={v}"))?; + let os_version = + Driver::get_os_version(opts.oci_ref).inspect(|v| trace!("os_version={v}"))?; let ref_name = get_env_var(GITHUB_REF_NAME).inspect(|v| trace!("{GITHUB_REF_NAME}={v}"))?; let short_sha = { let mut short_sha = get_env_var(GITHUB_SHA).inspect(|v| trace!("{GITHUB_SHA}={v}"))?; @@ -47,7 +48,7 @@ impl CiDriver for GithubDriver { let tags = match ( Self::on_default_branch(), - recipe.alt_tags.as_ref(), + opts.alt_tags.as_ref(), get_env_var(GITHUB_EVENT_NAME).inspect(|v| trace!("{GITHUB_EVENT_NAME}={v}")), get_env_var(PR_EVENT_NUMBER).inspect(|v| trace!("{PR_EVENT_NUMBER}={v}")), ) { @@ -128,21 +129,21 @@ impl CiDriver for GithubDriver { #[cfg(test)] mod test { - use blue_build_recipe::Recipe; + use std::borrow::Cow; + use blue_build_utils::{ constants::{ GITHUB_EVENT_NAME, GITHUB_EVENT_PATH, GITHUB_REF_NAME, GITHUB_SHA, PR_EVENT_NUMBER, }, - string_vec, + cowstr_vec, string_vec, test_utils::set_env_var, }; + use oci_distribution::Reference; use rstest::rstest; use crate::{ - drivers::CiDriver, - test::{ - create_test_recipe, create_test_recipe_alt_tags, TEST_TAG_1, TEST_TAG_2, TIMESTAMP, - }, + drivers::{opts::GenerateTagsOpts, CiDriver}, + test::{TEST_TAG_1, TEST_TAG_2, TIMESTAMP}, }; use super::GithubDriver; @@ -216,7 +217,7 @@ mod test { #[rstest] #[case::default_branch( setup_default_branch, - create_test_recipe, + None, string_vec![ format!("{}-40", &*TIMESTAMP), "latest", @@ -227,7 +228,7 @@ mod test { )] #[case::default_branch_alt_tags( setup_default_branch, - create_test_recipe_alt_tags, + Some(cowstr_vec![TEST_TAG_1, TEST_TAG_2]), string_vec![ TEST_TAG_1, format!("{TEST_TAG_1}-40"), @@ -241,12 +242,12 @@ mod test { )] #[case::pr_branch( setup_pr_branch, - create_test_recipe, + None, string_vec!["pr-12-40", format!("{COMMIT_SHA}-40")], )] #[case::pr_branch_alt_tags( setup_pr_branch, - create_test_recipe_alt_tags, + Some(cowstr_vec![TEST_TAG_1, TEST_TAG_2]), string_vec![ format!("pr-12-{TEST_TAG_1}-40"), format!("{COMMIT_SHA}-{TEST_TAG_1}-40"), @@ -256,12 +257,12 @@ mod test { )] #[case::branch( setup_branch, - create_test_recipe, + None, string_vec![format!("{COMMIT_SHA}-40"), "br-test-40"], )] #[case::branch_alt_tags( setup_branch, - create_test_recipe_alt_tags, + Some(cowstr_vec![TEST_TAG_1, TEST_TAG_2]), string_vec![ format!("br-{BR_REF_NAME}-{TEST_TAG_1}-40"), format!("{COMMIT_SHA}-{TEST_TAG_1}-40"), @@ -271,14 +272,20 @@ mod test { )] fn generate_tags( #[case] setup: impl FnOnce(), - #[case] recipe_fn: impl Fn() -> Recipe<'static>, + #[case] alt_tags: Option>>, #[case] mut expected: Vec, ) { setup(); expected.sort(); - let recipe = recipe_fn(); - - let mut tags = GithubDriver::generate_tags(&recipe).unwrap(); + let oci_ref: Reference = "ghcr.io/ublue-os/silverblue-main".parse().unwrap(); + + let mut tags = GithubDriver::generate_tags( + &GenerateTagsOpts::builder() + .oci_ref(&oci_ref) + .alt_tags(alt_tags) + .build(), + ) + .unwrap(); tags.sort(); assert_eq!(tags, expected); diff --git a/process/drivers/gitlab_driver.rs b/process/drivers/gitlab_driver.rs index e7d5db57..bc881ac8 100644 --- a/process/drivers/gitlab_driver.rs +++ b/process/drivers/gitlab_driver.rs @@ -16,7 +16,7 @@ use blue_build_utils::test_utils::get_env_var; use crate::drivers::Driver; -use super::CiDriver; +use super::{opts::GenerateTagsOpts, CiDriver}; pub struct GitlabDriver; @@ -43,9 +43,9 @@ impl CiDriver for GitlabDriver { )) } - fn generate_tags(recipe: &blue_build_recipe::Recipe) -> miette::Result> { + fn generate_tags(opts: &GenerateTagsOpts) -> miette::Result> { const MR_EVENT: &str = "merge_request_event"; - let os_version = Driver::get_os_version(recipe)?; + let os_version = Driver::get_os_version(opts.oci_ref)?; let timestamp = blue_build_utils::get_tag_timestamp(); let short_sha = get_env_var(CI_COMMIT_SHORT_SHA).inspect(|v| trace!("{CI_COMMIT_SHORT_SHA}={v}"))?; @@ -54,7 +54,7 @@ impl CiDriver for GitlabDriver { let tags = match ( Self::on_default_branch(), - recipe.alt_tags.as_ref(), + opts.alt_tags.as_ref(), get_env_var(CI_MERGE_REQUEST_IID).inspect(|v| trace!("{CI_MERGE_REQUEST_IID}={v}")), get_env_var(CI_PIPELINE_SOURCE).inspect(|v| trace!("{CI_PIPELINE_SOURCE}={v}")), ) { @@ -141,23 +141,23 @@ impl CiDriver for GitlabDriver { #[cfg(test)] mod test { - use blue_build_recipe::Recipe; + use std::borrow::Cow; + use blue_build_utils::{ constants::{ CI_COMMIT_REF_NAME, CI_COMMIT_SHORT_SHA, CI_DEFAULT_BRANCH, CI_MERGE_REQUEST_IID, CI_PIPELINE_SOURCE, CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_REGISTRY, CI_SERVER_HOST, CI_SERVER_PROTOCOL, }, - string_vec, + cowstr_vec, string_vec, test_utils::set_env_var, }; + use oci_distribution::Reference; use rstest::rstest; use crate::{ - drivers::CiDriver, - test::{ - create_test_recipe, create_test_recipe_alt_tags, TEST_TAG_1, TEST_TAG_2, TIMESTAMP, - }, + drivers::{opts::GenerateTagsOpts, CiDriver}, + test::{TEST_TAG_1, TEST_TAG_2, TIMESTAMP}, }; use super::GitlabDriver; @@ -227,7 +227,7 @@ mod test { #[rstest] #[case::default_branch( setup_default_branch, - create_test_recipe, + None, string_vec![ format!("{}-40", &*TIMESTAMP), "latest", @@ -238,7 +238,7 @@ mod test { )] #[case::default_branch_alt_tags( setup_default_branch, - create_test_recipe_alt_tags, + Some(cowstr_vec![TEST_TAG_1, TEST_TAG_2]), string_vec![ TEST_TAG_1, format!("{TEST_TAG_1}-40"), @@ -252,12 +252,12 @@ mod test { )] #[case::pr_branch( setup_mr_branch, - create_test_recipe, + None, string_vec!["mr-12-40", format!("{COMMIT_SHA}-40")], )] #[case::pr_branch_alt_tags( setup_mr_branch, - create_test_recipe_alt_tags, + Some(cowstr_vec![TEST_TAG_1, TEST_TAG_2]), string_vec![ format!("mr-12-{TEST_TAG_1}-40"), format!("{COMMIT_SHA}-{TEST_TAG_1}-40"), @@ -267,12 +267,12 @@ mod test { )] #[case::branch( setup_branch, - create_test_recipe, + None, string_vec![format!("{COMMIT_SHA}-40"), "br-test-40"], )] #[case::branch_alt_tags( setup_branch, - create_test_recipe_alt_tags, + Some(cowstr_vec![TEST_TAG_1, TEST_TAG_2]), string_vec![ format!("br-{BR_REF_NAME}-{TEST_TAG_1}-40"), format!("{COMMIT_SHA}-{TEST_TAG_1}-40"), @@ -282,14 +282,20 @@ mod test { )] fn generate_tags( #[case] setup: impl FnOnce(), - #[case] recipe_fn: impl Fn() -> Recipe<'static>, + #[case] alt_tags: Option>>, #[case] mut expected: Vec, ) { setup(); expected.sort(); - let recipe = recipe_fn(); + let oci_ref: Reference = "ghcr.io/ublue-os/silverblue-main".parse().unwrap(); - let mut tags = GitlabDriver::generate_tags(&recipe).unwrap(); + let mut tags = GitlabDriver::generate_tags( + &GenerateTagsOpts::builder() + .oci_ref(&oci_ref) + .alt_tags(alt_tags) + .build(), + ) + .unwrap(); tags.sort(); assert_eq!(tags, expected); diff --git a/process/drivers/local_driver.rs b/process/drivers/local_driver.rs index 625f69ae..2f53cf14 100644 --- a/process/drivers/local_driver.rs +++ b/process/drivers/local_driver.rs @@ -1,7 +1,9 @@ +use blue_build_utils::string_vec; use log::trace; -use miette::bail; +use miette::{bail, Context, IntoDiagnostic}; +use oci_distribution::Reference; -use super::{CiDriver, Driver}; +use super::{opts::GenerateTagsOpts, CiDriver, Driver}; pub struct LocalDriver; @@ -21,14 +23,31 @@ impl CiDriver for LocalDriver { bail!("Keyless not supported"); } - fn generate_tags(recipe: &blue_build_recipe::Recipe) -> miette::Result> { - trace!("LocalDriver::generate_tags({recipe:?})"); - Ok(vec![format!("local-{}", Driver::get_os_version(recipe)?)]) + fn generate_tags(opts: &GenerateTagsOpts) -> miette::Result> { + trace!("LocalDriver::generate_tags({opts:?})"); + let os_version = Driver::get_os_version(opts.oci_ref)?; + Ok(opts.alt_tags.as_ref().map_or_else( + || string_vec![format!("local-{os_version}")], + |alt_tags| { + alt_tags + .iter() + .flat_map(|alt| string_vec![format!("local-{alt}-{os_version}")]) + .collect() + }, + )) } - fn generate_image_name(recipe: &blue_build_recipe::Recipe) -> miette::Result { - trace!("LocalDriver::generate_image_name({recipe:?})"); - Ok(recipe.name.trim().to_lowercase()) + fn generate_image_name(name: S) -> miette::Result + where + S: AsRef, + { + fn inner(name: &str) -> miette::Result { + trace!("LocalDriver::generate_image_name({name})"); + name.parse() + .into_diagnostic() + .with_context(|| format!("Unable to parse {name}")) + } + inner(&name.as_ref().trim().to_lowercase()) } fn get_repo_url() -> miette::Result { diff --git a/process/drivers/opts.rs b/process/drivers/opts.rs index 824c01de..3cbd025b 100644 --- a/process/drivers/opts.rs +++ b/process/drivers/opts.rs @@ -1,11 +1,13 @@ use clap::ValueEnum; pub use build::*; +pub use ci::*; pub use inspect::*; pub use run::*; pub use signing::*; mod build; +mod ci; mod inspect; mod run; mod signing; diff --git a/process/drivers/opts/build.rs b/process/drivers/opts/build.rs index 77d4d194..3f4689da 100644 --- a/process/drivers/opts/build.rs +++ b/process/drivers/opts/build.rs @@ -41,6 +41,8 @@ pub struct PushOpts<'a> { pub struct BuildTagPushOpts<'a> { /// The base image name. /// + /// NOTE: This SHOULD NOT contain the tag of the image. + /// /// NOTE: You cannot have this set with `archive_path` set. #[builder(default, setter(into, strip_option))] pub image: Option>, diff --git a/process/drivers/opts/ci.rs b/process/drivers/opts/ci.rs new file mode 100644 index 00000000..29138adb --- /dev/null +++ b/process/drivers/opts/ci.rs @@ -0,0 +1,12 @@ +use std::borrow::Cow; + +use oci_distribution::Reference; +use typed_builder::TypedBuilder; + +#[derive(Debug, Clone, TypedBuilder)] +pub struct GenerateTagsOpts<'scope> { + pub oci_ref: &'scope Reference, + + #[builder(default, setter(into))] + pub alt_tags: Option>>, +} diff --git a/process/drivers/opts/run.rs b/process/drivers/opts/run.rs index ab2a2fb6..6c0971a6 100644 --- a/process/drivers/opts/run.rs +++ b/process/drivers/opts/run.rs @@ -11,10 +11,10 @@ pub struct RunOpts<'scope> { pub args: Cow<'scope, [String]>, #[builder(default, setter(into))] - pub env_vars: Cow<'scope, [RunOptsEnv<'scope>]>, + pub env_vars: Vec>, #[builder(default, setter(into))] - pub volumes: Cow<'scope, [RunOptsVolume<'scope>]>, + pub volumes: Vec>, #[builder(default, setter(strip_option))] pub uid: Option, @@ -48,7 +48,7 @@ pub struct RunOptsVolume<'scope> { macro_rules! run_volumes { ($($host:expr => $container:expr),+ $(,)?) => { { - [ + vec![ $($crate::drivers::opts::RunOptsVolume::builder() .path_or_vol_name($host) .container_path($container) @@ -71,7 +71,7 @@ pub struct RunOptsEnv<'scope> { macro_rules! run_envs { ($($key:expr => $value:expr),+ $(,)?) => { { - [ + vec![ $($crate::drivers::opts::RunOptsEnv::builder() .key($key) .value($value) diff --git a/process/drivers/podman_driver.rs b/process/drivers/podman_driver.rs index d8eb5ab6..3405c6d6 100644 --- a/process/drivers/podman_driver.rs +++ b/process/drivers/podman_driver.rs @@ -227,8 +227,12 @@ impl RunDriver for PodmanDriver { add_cid(&cid); - let status = podman_run(opts, &cid_file) - .status_image_ref_progress(&*opts.image, "Running container")?; + let status = if opts.privileged { + podman_run(opts, &cid_file).status()? + } else { + podman_run(opts, &cid_file) + .status_image_ref_progress(&*opts.image, "Running container")? + }; remove_cid(&cid); @@ -254,7 +258,7 @@ impl RunDriver for PodmanDriver { } fn podman_run(opts: &RunOpts, cid_file: &Path) -> Command { - cmd!( + let command = cmd!( if opts.privileged { warn!( "Running 'podman' in privileged mode requires '{}'", @@ -267,7 +271,10 @@ fn podman_run(opts: &RunOpts, cid_file: &Path) -> Command { if opts.privileged => "podman", "run", format!("--cidfile={}", cid_file.display()), - if opts.privileged => "--privileged", + if opts.privileged => [ + "--privileged", + "--network=host", + ], if opts.remove => "--rm", if opts.pull => "--pull=always", for volume in opts.volumes => [ @@ -280,5 +287,8 @@ fn podman_run(opts: &RunOpts, cid_file: &Path) -> Command { ], &*opts.image, for opts.args, - ) + ); + trace!("{command:?}"); + + command } diff --git a/process/drivers/traits.rs b/process/drivers/traits.rs index fcacc996..22f84876 100644 --- a/process/drivers/traits.rs +++ b/process/drivers/traits.rs @@ -3,10 +3,10 @@ use std::{ process::{ExitStatus, Output}, }; -use blue_build_recipe::Recipe; use blue_build_utils::{constants::COSIGN_PUB_PATH, retry}; use log::{debug, info, trace}; -use miette::{bail, miette, Result}; +use miette::{bail, miette, Context, IntoDiagnostic, Result}; +use oci_distribution::Reference; use semver::{Version, VersionReq}; use crate::drivers::{functions::get_private_key, types::CiDriverType, Driver}; @@ -14,8 +14,9 @@ use crate::drivers::{functions::get_private_key, types::CiDriverType, Driver}; use super::{ image_metadata::ImageMetadata, opts::{ - BuildOpts, BuildTagPushOpts, CheckKeyPairOpts, GenerateKeyPairOpts, GetMetadataOpts, - PushOpts, RunOpts, SignOpts, SignVerifyOpts, TagOpts, VerifyOpts, VerifyType, + BuildOpts, BuildTagPushOpts, CheckKeyPairOpts, GenerateKeyPairOpts, GenerateTagsOpts, + GetMetadataOpts, PushOpts, RunOpts, SignOpts, SignVerifyOpts, TagOpts, VerifyOpts, + VerifyType, }, }; @@ -307,18 +308,27 @@ pub trait CiDriver { /// /// # Errors /// Will error if the environment variables aren't set. - fn generate_tags(recipe: &Recipe) -> Result>; + fn generate_tags(oci_ref: &GenerateTagsOpts) -> Result>; /// Generates the image name based on CI. /// /// # Errors /// Will error if the environment variables aren't set. - fn generate_image_name(recipe: &Recipe) -> Result { - Ok(format!( - "{}/{}", - Self::get_registry()?, - recipe.name.trim().to_lowercase() - )) + fn generate_image_name(name: S) -> Result + where + S: AsRef, + { + fn inner(name: &str, registry: &str) -> Result { + let image = format!("{registry}/{name}"); + image + .parse() + .into_diagnostic() + .with_context(|| format!("Unable to parse image {image}")) + } + inner( + &name.as_ref().trim().to_lowercase(), + &Self::get_registry()?.to_lowercase(), + ) } /// Get the URL for the repository. diff --git a/process/process.rs b/process/process.rs index 0349172a..100bc6ee 100644 --- a/process/process.rs +++ b/process/process.rs @@ -23,45 +23,8 @@ pub(crate) static RT: Lazy = Lazy::new(|| { pub(crate) mod test { use std::sync::LazyLock; - use blue_build_recipe::{Module, ModuleExt, Recipe}; - use blue_build_utils::cowstr_vec; - use indexmap::IndexMap; - pub const TEST_TAG_1: &str = "test-tag-1"; pub const TEST_TAG_2: &str = "test-tag-2"; pub static TIMESTAMP: LazyLock = LazyLock::new(blue_build_utils::get_tag_timestamp); - - pub fn create_test_recipe() -> Recipe<'static> { - Recipe::builder() - .name("test") - .description("This is a test") - .base_image("base-image") - .image_version("40") - .modules_ext( - ModuleExt::builder() - .modules(vec![Module::builder().build()]) - .build(), - ) - .stages_ext(None) - .extra(IndexMap::new()) - .build() - } - - pub fn create_test_recipe_alt_tags() -> Recipe<'static> { - Recipe::builder() - .name("test") - .description("This is a test") - .base_image("base-image") - .image_version("40") - .alt_tags(cowstr_vec![TEST_TAG_1, TEST_TAG_2]) - .modules_ext( - ModuleExt::builder() - .modules(vec![Module::builder().build()]) - .build(), - ) - .stages_ext(None) - .extra(IndexMap::new()) - .build() - } } diff --git a/recipe/Cargo.toml b/recipe/Cargo.toml index 5e376be7..9e7af97c 100644 --- a/recipe/Cargo.toml +++ b/recipe/Cargo.toml @@ -14,6 +14,7 @@ blue-build-utils = { version = "=0.8.14", path = "../utils" } colored.workspace = true log.workspace = true miette.workspace = true +oci-distribution.workspace = true indexmap.workspace = true serde.workspace = true serde_yaml.workspace = true diff --git a/recipe/src/recipe.rs b/recipe/src/recipe.rs index 74a94d71..02a6707f 100644 --- a/recipe/src/recipe.rs +++ b/recipe/src/recipe.rs @@ -1,8 +1,10 @@ use std::{borrow::Cow, fs, path::Path}; +use blue_build_utils::cowstr; use indexmap::IndexMap; use log::{debug, trace}; use miette::{Context, IntoDiagnostic, Result}; +use oci_distribution::Reference; use serde::{Deserialize, Serialize}; use serde_yaml::Value; use typed_builder::TypedBuilder; @@ -53,7 +55,7 @@ pub struct Recipe<'a> { /// Any user input will override the `latest` and timestamp tags. #[serde(alias = "alt-tags", skip_serializing_if = "Option::is_none")] #[builder(default, setter(into, strip_option))] - pub alt_tags: Option>>, + alt_tags: Option>>, /// The stages extension of the recipe. /// @@ -118,4 +120,22 @@ impl<'a> Recipe<'a> { Ok(recipe) } + + /// Get a `Reference` object of the `base_image`. + /// + /// # Errors + /// Will error if it fails to parse the `base_image`. + pub fn base_image_ref(&self) -> Result { + self.base_image + .parse() + .into_diagnostic() + .with_context(|| format!("Unable to parse base image {}", self.base_image)) + } + + #[must_use] + pub fn alt_tags(&'a self) -> Option>> { + self.alt_tags + .as_ref() + .map(|tags| tags.iter().map(|tag| cowstr!(&**tag)).collect()) + } } diff --git a/recipe/src/stage.rs b/recipe/src/stage.rs index 65f65c53..23224f3e 100644 --- a/recipe/src/stage.rs +++ b/recipe/src/stage.rs @@ -27,7 +27,7 @@ pub struct StageRequiredFields<'a> { /// The shell to use in the stage. #[builder(default, setter(into, strip_option))] #[serde(skip_serializing_if = "Option::is_none")] - pub shell: Option>>, + pub shell: Option]>>, /// The modules extension for the stage #[serde(flatten)] diff --git a/src/bin/bluebuild.rs b/src/bin/bluebuild.rs index 1c37dd34..06776492 100644 --- a/src/bin/bluebuild.rs +++ b/src/bin/bluebuild.rs @@ -39,6 +39,9 @@ fn main() { #[cfg(feature = "login")] CommandArgs::Login(mut command) => command.run(), + #[cfg(feature = "iso")] + CommandArgs::GenerateIso(mut command) => command.run(), + CommandArgs::BugReport(mut command) => command.run(), CommandArgs::Completions(mut command) => command.run(), diff --git a/src/commands.rs b/src/commands.rs index 327fddb5..5c6c2b97 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -11,6 +11,8 @@ pub mod bug_report; pub mod build; pub mod completions; pub mod generate; +#[cfg(feature = "iso")] +pub mod generate_iso; #[cfg(feature = "login")] pub mod login; // #[cfg(feature = "init")] @@ -68,6 +70,10 @@ pub enum CommandArgs { #[clap(visible_alias = "template")] Generate(generate::GenerateCommand), + /// Generate an ISO for an image or recipe. + #[cfg(feature = "iso")] + GenerateIso(generate_iso::GenerateIsoCommand), + /// Upgrade your current OS with the /// local image saved at `/etc/bluebuild/`. /// diff --git a/src/commands/build.rs b/src/commands/build.rs index 89055f98..e884a500 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -4,7 +4,7 @@ use std::{ }; use blue_build_process_management::drivers::{ - opts::{BuildTagPushOpts, CheckKeyPairOpts, CompressionType}, + opts::{BuildTagPushOpts, CheckKeyPairOpts, CompressionType, GenerateTagsOpts, SignVerifyOpts}, BuildDriver, CiDriver, Driver, DriverArgs, SigningDriver, }; use blue_build_recipe::Recipe; @@ -14,11 +14,13 @@ use blue_build_utils::{ GITIGNORE_PATH, LABELED_ERROR_MESSAGE, NO_LABEL_ERROR_MESSAGE, RECIPE_FILE, RECIPE_PATH, }, credentials::{Credentials, CredentialsArgs}, + string, }; use clap::Args; use colored::Colorize; use log::{debug, info, trace, warn}; use miette::{bail, Context, IntoDiagnostic, Result}; +use oci_distribution::Reference; use typed_builder::TypedBuilder; use crate::commands::generate::GenerateCommand; @@ -197,7 +199,6 @@ impl BlueBuildCommand for BuildCommand { impl BuildCommand { #[cfg(feature = "multi-recipe")] fn start(&self, recipe_paths: &[PathBuf]) -> Result<()> { - use blue_build_process_management::drivers::opts::SignVerifyOpts; use rayon::prelude::*; trace!("BuildCommand::build_image()"); @@ -205,54 +206,12 @@ impl BuildCommand { recipe_paths .par_iter() .try_for_each(|recipe_path| -> Result<()> { - let recipe = Recipe::parse(recipe_path)?; let containerfile = if recipe_paths.len() > 1 { blue_build_utils::generate_containerfile_path(recipe_path)? } else { PathBuf::from(CONTAINER_FILE) }; - let tags = Driver::generate_tags(&recipe)?; - let image_name = self.generate_full_image_name(&recipe)?; - - let opts = if let Some(archive_dir) = self.archive.as_ref() { - BuildTagPushOpts::builder() - .containerfile(&containerfile) - .archive_path(format!( - "{}/{}.{ARCHIVE_SUFFIX}", - archive_dir.to_string_lossy().trim_end_matches('/'), - recipe.name.to_lowercase().replace('/', "_"), - )) - .squash(self.squash) - .build() - } else { - BuildTagPushOpts::builder() - .image(&image_name) - .containerfile(&containerfile) - .tags(&tags) - .push(self.push) - .retry_push(self.retry_push) - .retry_count(self.retry_count) - .compression(self.compression_format) - .squash(self.squash) - .build() - }; - - Driver::build_tag_push(&opts)?; - - if self.push && !self.no_sign { - let opts = SignVerifyOpts::builder() - .image(&image_name) - .retry_push(self.retry_push) - .retry_count(self.retry_count); - let opts = if let Some(tag) = tags.first() { - opts.tag(tag).build() - } else { - opts.build() - }; - Driver::sign_and_verify(&opts)?; - } - - Ok(()) + self.build(recipe_path, &containerfile) })?; info!("Build complete!"); @@ -261,18 +220,27 @@ impl BuildCommand { #[cfg(not(feature = "multi-recipe"))] fn start(&self, recipe_path: &Path) -> Result<()> { - use blue_build_process_management::drivers::opts::SignVerifyOpts; - trace!("BuildCommand::start()"); + self.build(recipe_path, Path::new(CONTAINER_FILE))?; + + info!("Build complete!"); + Ok(()) + } + + fn build(&self, recipe_path: &Path, containerfile: &Path) -> Result<()> { let recipe = Recipe::parse(recipe_path)?; - let containerfile = PathBuf::from(CONTAINER_FILE); - let tags = Driver::generate_tags(&recipe)?; - let image_name = self.generate_full_image_name(&recipe)?; + let tags = Driver::generate_tags( + &GenerateTagsOpts::builder() + .oci_ref(&recipe.base_image_ref()?) + .alt_tags(recipe.alt_tags()) + .build(), + )?; + let image_name = self.image_name(&recipe)?; let opts = if let Some(archive_dir) = self.archive.as_ref() { BuildTagPushOpts::builder() - .containerfile(&containerfile) + .containerfile(containerfile) .archive_path(format!( "{}/{}.{ARCHIVE_SUFFIX}", archive_dir.to_string_lossy().trim_end_matches('/'), @@ -283,7 +251,7 @@ impl BuildCommand { } else { BuildTagPushOpts::builder() .image(&image_name) - .containerfile(&containerfile) + .containerfile(containerfile) .tags(&tags) .push(self.push) .retry_push(self.retry_push) @@ -308,14 +276,29 @@ impl BuildCommand { Driver::sign_and_verify(&opts)?; } - info!("Build complete!"); Ok(()) } + fn image_name(&self, recipe: &Recipe) -> Result { + let image_name = self.generate_full_image_name(recipe)?; + + let image_name = if image_name.registry().is_empty() { + string!(image_name.repository()) + } else { + format!( + "{}/{}", + image_name.resolve_registry(), + image_name.repository() + ) + }; + + Ok(image_name) + } + /// # Errors /// /// Will return `Err` if the image name cannot be generated. - fn generate_full_image_name(&self, recipe: &Recipe) -> Result { + fn generate_full_image_name(&self, recipe: &Recipe) -> Result { trace!("BuildCommand::generate_full_image_name({recipe:#?})"); info!("Generating full image name"); @@ -324,14 +307,18 @@ impl BuildCommand { self.registry_namespace.as_ref().map(|s| s.to_lowercase()), ) { trace!("registry={registry}, registry_path={registry_path}"); - format!( + let image = format!( "{}/{}/{}", registry.trim().trim_matches('/'), registry_path.trim().trim_matches('/'), recipe.name.trim(), - ) + ); + image + .parse() + .into_diagnostic() + .with_context(|| format!("Unable to parse {image}"))? } else { - Driver::generate_image_name(recipe)? + Driver::generate_image_name(&recipe.name)? }; debug!("Using image name '{image_name}'"); diff --git a/src/commands/generate.rs b/src/commands/generate.rs index b6fec42d..c3581f11 100644 --- a/src/commands/generate.rs +++ b/src/commands/generate.rs @@ -93,15 +93,15 @@ impl GenerateCommand { }); debug!("Deserializing recipe"); - let recipe_de = Recipe::parse(&recipe_path)?; - trace!("recipe_de: {recipe_de:#?}"); + let recipe = Recipe::parse(&recipe_path)?; + trace!("recipe_de: {recipe:#?}"); if self.display_full_recipe { if let Some(output) = self.output.as_ref() { - std::fs::write(output, serde_yaml::to_string(&recipe_de).into_diagnostic()?) + std::fs::write(output, serde_yaml::to_string(&recipe).into_diagnostic()?) .into_diagnostic()?; } else { - syntax_highlighting::print_ser(&recipe_de, "yml", self.syntax_theme)?; + syntax_highlighting::print_ser(&recipe, "yml", self.syntax_theme)?; } return Ok(()); } @@ -109,9 +109,9 @@ impl GenerateCommand { info!("Templating for recipe at {}", recipe_path.display()); let template = ContainerFileTemplate::builder() - .os_version(Driver::get_os_version(&recipe_de)?) + .os_version(Driver::get_os_version(&recipe.base_image_ref()?)?) .build_id(Driver::get_build_id()) - .recipe(&recipe_de) + .recipe(&recipe) .recipe_path(recipe_path.as_path()) .registry(Driver::get_registry()?) .repo(Driver::get_repo_url()?) @@ -142,7 +142,3 @@ impl GenerateCommand { Ok(()) } } - -// ======================================================== // -// ========================= Helpers ====================== // -// ======================================================== // diff --git a/src/commands/generate_iso.rs b/src/commands/generate_iso.rs new file mode 100644 index 00000000..29a103b0 --- /dev/null +++ b/src/commands/generate_iso.rs @@ -0,0 +1,240 @@ +use std::{ + env, fs, + path::{self, Path, PathBuf}, +}; + +use blue_build_recipe::Recipe; +use blue_build_utils::{constants::ARCHIVE_SUFFIX, string_vec}; +use clap::{Args, Subcommand, ValueEnum}; +use miette::{bail, Context, IntoDiagnostic, Result}; +use oci_distribution::Reference; +use tempdir::TempDir; +use typed_builder::TypedBuilder; + +use blue_build_process_management::{ + drivers::{opts::RunOpts, Driver, DriverArgs, RunDriver}, + run_volumes, +}; + +use super::{build::BuildCommand, BlueBuildCommand}; + +#[derive(Clone, Debug, TypedBuilder, Args)] +pub struct GenerateIsoCommand { + #[command(subcommand)] + command: GenIsoSubcommand, + + /// The directory to save the resulting ISO file. + #[arg(short, long)] + output_dir: Option, + + /// The variant of the installer to use. + /// + /// The Kinoite variant will ask for a user + /// and password before installing the OS. + /// This version is the most stable and is + /// recommended. + /// + /// The Silverblue variant will ask for a user + /// and password on first boot after the OS + /// is installed. + /// + /// The Server variant is the basic installer + /// and will ask to setup a user at install time. + #[arg(short = 'V', long, default_value = "server")] + variant: GenIsoVariant, + + /// The url to the secure boot public key. + /// + /// Defaults to one of UBlue's public key. + /// It's recommended to change this if your base + /// image is not from UBlue. + #[arg( + long, + default_value = "https://github.com/ublue-os/bazzite/raw/main/secure_boot.der" + )] + secure_boot_url: String, + + /// The enrollment password for the secure boot + /// key. + /// + /// Default's to UBlue's enrollment password. + /// It's recommended to change this if your base + /// image is not from UBlue. + #[arg(long, default_value = "universalblue")] + enrollment_password: String, + + /// The name of your ISO image file. + #[arg(long)] + iso_name: Option, + + #[clap(flatten)] + #[builder(default)] + drivers: DriverArgs, +} + +#[derive(Debug, Clone, Subcommand)] +pub enum GenIsoSubcommand { + /// Build an ISO from a remote image. + Image { + /// The image ref to create the iso from. + #[arg()] + image: String, + }, + /// Build an ISO from a recipe. + /// + /// This will build the image locally first + /// before creating the ISO. This is a long + /// process. + Recipe { + /// The path to the recipe file for your image. + #[arg()] + recipe: PathBuf, + }, +} + +#[derive(Debug, Default, Clone, Copy, ValueEnum)] +pub enum GenIsoVariant { + #[default] + Kinoite, + Silverblue, + Server, +} + +impl std::fmt::Display for GenIsoVariant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match *self { + Self::Server => "Server", + Self::Silverblue => "Silverblue", + Self::Kinoite => "Kinoite", + } + ) + } +} + +impl BlueBuildCommand for GenerateIsoCommand { + fn try_run(&mut self) -> Result<()> { + Driver::init(self.drivers); + + let image_out_dir = TempDir::new("build_image").into_diagnostic()?; + + let output_dir = if let Some(output_dir) = self.output_dir.clone() { + if output_dir.exists() && !output_dir.is_dir() { + bail!("The '--output-dir' arg must be a directory"); + } + + if !output_dir.exists() { + fs::create_dir(&output_dir).into_diagnostic()?; + } + + path::absolute(output_dir).into_diagnostic()? + } else { + env::current_dir().into_diagnostic()? + }; + + if let GenIsoSubcommand::Recipe { recipe } = &self.command { + #[cfg(feature = "multi-recipe")] + let mut build_command = { + BuildCommand::builder() + .recipe(vec![recipe.clone()]) + .archive(image_out_dir.path()) + .build() + }; + #[cfg(not(feature = "multi-recipe"))] + let mut build_command = { + BuildCommand::builder() + .recipe(recipe.to_path_buf()) + .archive(image_out_dir.path()) + .build() + }; + + build_command.try_run()?; + } + + let iso_name = self.iso_name.as_ref().map_or("deploy.iso", String::as_str); + let iso_path = output_dir.join(iso_name); + + if iso_path.exists() { + fs::remove_file(iso_path).into_diagnostic()?; + } + + self.build_iso(iso_name, &output_dir, image_out_dir.path()) + } +} + +impl GenerateIsoCommand { + fn build_iso(&self, iso_name: &str, output_dir: &Path, image_out_dir: &Path) -> Result<()> { + let mut args = string_vec![ + format!("VARIANT={}", self.variant), + format!("ISO_NAME=build/{iso_name}"), + "DNF_CACHE=/cache/dnf", + format!("SECURE_BOOT_KEY_URL={}", self.secure_boot_url), + format!("ENROLLMENT_PASSWORD={}", self.enrollment_password), + ]; + let mut vols = run_volumes![ + output_dir.display().to_string() => "/build-container-installer/build", + "dnf-cache" => "/cache/dnf/", + ]; + + match &self.command { + GenIsoSubcommand::Image { image } => { + let image: Reference = image + .parse() + .into_diagnostic() + .with_context(|| format!("Unable to parse image reference {image}"))?; + let (image_repo, image_name) = { + let registry = image.resolve_registry(); + let repo = image.repository(); + let image = format!("{registry}/{repo}"); + + let mut image_parts = image.split('/').collect::>(); + let image_name = image_parts.pop().unwrap(); // Should be at least 2 elements + let image_repo = image_parts.join("/"); + (image_repo, image_name.to_string()) + }; + + args.extend([ + format!("IMAGE_NAME={image_name}",), + format!("IMAGE_REPO={image_repo}"), + format!("IMAGE_TAG={}", image.tag().unwrap_or("latest")), + format!("VERSION={}", Driver::get_os_version(&image)?), + ]); + } + GenIsoSubcommand::Recipe { recipe } => { + let recipe = Recipe::parse(recipe)?; + + args.extend([ + format!( + "IMAGE_SRC=oci-archive:/img_src/{}.{ARCHIVE_SUFFIX}", + recipe.name.replace('/', "_"), + ), + format!( + "VERSION={}", + Driver::get_os_version(&recipe.base_image_ref()?)?, + ), + ]); + vols.extend(run_volumes![ + image_out_dir.display().to_string() => "/img_src/", + ]); + } + } + + // Currently testing local tarball builds + let opts = RunOpts::builder() + .image("ghcr.io/jasonn3/build-container-installer") + .privileged(true) + .remove(true) + .args(&args) + .volumes(vols) + .build(); + + let status = Driver::run(&opts).into_diagnostic()?; + + if !status.success() { + bail!("Failed to create ISO"); + } + Ok(()) + } +}