diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c6f00e0..887cc40 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,3 +1,3 @@ include: - project: wunker-bunker/ci-pipelines - file: cargo-build.yml + file: cargo-earthly.yml diff --git a/Cargo.lock b/Cargo.lock index 0c61367..12bc0f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,6 +165,7 @@ dependencies = [ "serde_json", "serde_yaml", "typed-builder", + "users", ] [[package]] @@ -817,6 +818,16 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + [[package]] name = "utf8parse" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 21a9028..4f0fda5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,10 +23,11 @@ serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.107" serde_yaml = "0.9.25" typed-builder = "0.18.0" +users = "0.11.0" [features] default = ["build"] -nightly = ["init", "build"] +nightly = ["build"] init = [] build = [] diff --git a/Earthfile b/Earthfile index ea5d9c3..4eced68 100644 --- a/Earthfile +++ b/Earthfile @@ -1,12 +1,119 @@ -VERSION --global-cache 0.7 -IMPORT github.com/earthly/lib/rust AS rust +VERSION \ + --global-cache \ + --use-function-keyword \ + --arg-scope-and-set \ + 0.7 -ARG --global FEDORA_MAJOR_VERSION=38 +IMPORT gitlab.com/wunker-bunker/ci-pipelines/earthly/cargo AS cargo ARG --global IMAGE=registry.gitlab.com/wunker-bunker/blue-build +all: + BUILD +default + BUILD +nightly + +default: + WAIT + BUILD +lint + BUILD +test + END + BUILD +blue-build-cli + BUILD +blue-build-cli-alpine + BUILD +installer + +nightly: + WAIT + BUILD +lint --NIGHTLY=true + BUILD +test --NIGHTLY=true + END + BUILD +blue-build-cli --NIGHTLY=true + BUILD +blue-build-cli-alpine --NIGHTLY=true + BUILD +installer --NIGHTLY=true + +lint: + FROM +common + + ARG NIGHTLY=false + + DO cargo+LINT --NIGHTLY=$NIGHTLY + +test: + FROM +common + + ARG NIGHTLY=false + + DO cargo+TEST --NIGHTLY=$NIGHTLY + +install: + FROM +common + + ARG NIGHTLY=false + ARG --required BUILD_TARGET + + DO cargo+BUILD_RELEASE --BUILD_TARGET=$BUILD_TARGET --NIGHTLY=$NIGHTLY + + SAVE ARTIFACT target/$BUILD_TARGET/release/bb + +common: + FROM registry.gitlab.com/wunker-bunker/cargo-builder + + WORKDIR /app + COPY --keep-ts --dir src/ templates/ /app + COPY --keep-ts Cargo.* /app + COPY --keep-ts *.md /app + COPY --keep-ts LICENSE /app + + DO cargo+INIT + +blue-build-cli: + FROM registry.fedoraproject.org/fedora-toolbox + ARG NIGHTLY=false + + BUILD +install --BUILD_TARGET="x86_64-unknown-linux-gnu" --NIGHTLY=$NIGHTLY + + RUN dnf install --refresh -y buildah podman skopeo + + COPY +cosign/cosign /usr/bin/cosign + + COPY (+install/bb --BUILD_TARGET="x86_64-unknown-linux-gnu" --NIGHTLY=$NIGHTLY) /usr/bin/bb + + ARG TAG + ARG LATEST=false + DO cargo+SAVE_IMAGE --IMAGE=$IMAGE --TAG=$TAG --LATEST=$LATEST --NIGHTLY=$NIGHTLY + +blue-build-cli-alpine: + FROM alpine + ARG NIGHTLY=false + + BUILD +install --BUILD_TARGET="x86_64-unknown-linux-musl" --NIGHTLY=$NIGHTLY + + RUN apk update && apk add buildah podman skopeo fuse-overlayfs + + COPY +cosign/cosign /usr/bin/cosign + COPY (+install/bb --BUILD_TARGET="x86_64-unknown-linux-musl" --NIGHTLY=$NIGHTLY) /usr/bin/bb + + ARG TAG + ARG LATEST=false + DO cargo+SAVE_IMAGE --IMAGE=$IMAGE --TAG=$TAG --LATEST=$LATEST --NIGHTLY=$NIGHTLY --ALPINE=true + +installer: + FROM alpine + ARG NIGHTLY=false + + BUILD +install --BUILD_TARGET="x86_64-unknown-linux-gnu" --NIGHTLY=$NIGHTLY + + COPY (+install/bb --BUILD_TARGET="x86_64-unknown-linux-gnu") /out/bb + COPY install.sh /install.sh + + CMD ["cat", "/install.sh"] + + ARG TAG + ARG LATEST=false + ARG INSTALLER=true + DO cargo+SAVE_IMAGE --IMAGE=$IMAGE --TAG=$TAG --LATEST=$LATEST --NIGHTLY=$NIGHTLY --INSTALLER=$INSTALLER + iso-generator: - FROM registry.fedoraproject.org/fedora-toolbox:${FEDORA_MAJOR_VERSION} + FROM registry.fedoraproject.org/fedora-toolbox GIT CLONE https://github.com/ublue-os/isogenerator.git /isogenerator WORKDIR /isogenerator @@ -18,64 +125,3 @@ iso-generator: cosign: FROM gcr.io/projectsigstore/cosign SAVE ARTIFACT /ko-app/cosign - -install: - FROM rust - DO rust+INIT --keep_fingerprints=true - - COPY --keep-ts . /app - WORKDIR /app - - ARG --required TARGET - DO rust+CARGO --args="build --release --target $TARGET" --output="$TARGET/release/[^\./]+" - - SAVE ARTIFACT target/$TARGET/release/bb - -blue-build-cli: - FROM registry.fedoraproject.org/fedora-toolbox:${FEDORA_MAJOR_VERSION} - BUILD +install --TARGET="x86_64-unknown-linux-gnu" - - RUN dnf install --refresh -y buildah podman skopeo - - COPY +cosign/cosign /usr/bin/cosign - COPY (+install/bb --TARGET="x86_64-unknown-linux-gnu") /usr/bin/bb - - ARG TAG - IF [ "$TAG" != "" ] - SAVE IMAGE --push $IMAGE:$TAG - - ARG LATEST=false - - IF [ "$LATEST" = "true" ] - SAVE IMAGE --push $IMAGE:latest - END - ELSE - SAVE IMAGE blue-build - END - -blue-build-cli-alpine: - FROM alpine - BUILD +install --TARGET="x86_64-unknown-linux-musl" - - RUN apk update && apk add buildah podman skopeo fuse-overlayfs - - COPY +cosign/cosign /usr/bin/cosign - COPY (+install/bb --TARGET="x86_64-unknown-linux-musl") /usr/bin/bb - - ARG TAG - IF [ "$TAG" != "" ] - SAVE IMAGE --push $IMAGE:$TAG-alpine - - ARG LATEST=false - - IF [ "$LATEST" = "true" ] - SAVE IMAGE --push $IMAGE:alpine - END - ELSE - SAVE IMAGE blue-build:alpine - END - -all: - BUILD +blue-build-cli - BUILD +blue-build-cli-alpine - BUILD +iso-generator diff --git a/README.md b/README.md index 4472084..39e968a 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,22 @@ This is my personal project trying to create a more conise version of the [start ## Installation -Right now the only way to install this tool is to use `cargo`. +### Cargo + +This is the best way to install as it gives you the opportunity to bulid for your specific environment. ```bash cargo install --locked blue-build ``` +### Podman/Docker + +This will install the binary on your system in `/usr/local/bin`. This is only a `linux-gnu` version. + +```bash +podman run --rm registry.gitlab.com/wunker-bunker/blue-build:installer | sudo bash +``` + ## How to use ### Templating @@ -138,4 +148,4 @@ jobs: - [x] Setup pipeline automation for publishing - [ ] Create an init command to create a repo for you to start out - [ ] Setup the project to allow installing with `binstall` -- [ ] Create an install script for easy install for users without `cargo` +- [x] Create an install script for easy install for users without `cargo` diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..7fbcb1d --- /dev/null +++ b/install.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -euo pipefail + +function cleanup() { + echo "Cleaning up image" + podman stop -i -t 0 blue-build-installer + sleep 2 + podman image rm registry.gitlab.com/wunker-bunker/blue-build:installer +} + +podman pull registry.gitlab.com/wunker-bunker/blue-build:installer + +podman run -d --rm --name blue-build-installer registry.gitlab.com/wunker-bunker/blue-build:installer tail -f /dev/null + +set +e +podman cp blue-build-installer:/out/bb /usr/local/bin/bb + +RETVAL=$? +set -e + +if [ -n $RETVAL ]; then + cleanup + echo "Failed to copy file, try:" + printf "\tpodman run --rm registry.gitlab.com/wunker-bunker/blue-build:installer | sudo bash\n" + exit 1 +else + cleanup +fi + diff --git a/src/build.rs b/src/build.rs index 4eed174..d6a4d21 100644 --- a/src/build.rs +++ b/src/build.rs @@ -1,6 +1,6 @@ use std::{ env, fs, - path::PathBuf, + path::{Path, PathBuf}, process::{self, Command}, }; @@ -8,12 +8,15 @@ use anyhow::{anyhow, bail, Result}; use clap::Args; use log::{debug, error, info, trace, warn}; use typed_builder::TypedBuilder; +use users::{Users, UsersCache}; use crate::{ ops, template::{Recipe, TemplateCommand}, }; +const LOCAL_BUILD: &str = "/etc/blue-build"; + #[derive(Debug, Clone, Args, TypedBuilder)] pub struct BuildCommand { /// The recipe file to build an image @@ -25,6 +28,19 @@ pub struct BuildCommand { #[builder(default, setter(into))] containerfile: Option, + /// Rebase your current OS onto the image + /// being built. + /// + /// This will create a tarball of your image at + /// `/etc/blue-build/` and invoke `rpm-ostree` to + /// rebase onto the image using `oci-archive`. + /// + /// NOTE: This can only be used if you have `rpm-ostree` + /// installed and if the `--push` option isn't + /// used. This image will not be signed. + #[arg(short, long)] + rebase: bool, + /// Push the image with all the tags. /// /// Requires `--registry`, `--registry-path`, @@ -59,15 +75,32 @@ pub struct BuildCommand { } impl BuildCommand { + /// Runs the command and returns a result. pub fn try_run(&self) -> Result<()> { trace!("BuildCommand::try_run()"); + if self.push && self.rebase { + bail!("You cannot use '--rebase' and '--push' at the same time"); + } + if let Err(e1) = ops::check_command_exists("buildah") { ops::check_command_exists("podman").map_err(|e2| { anyhow!("Need either 'buildah' or 'podman' commands to proceed: {e1}, {e2}") })?; } + if self.rebase { + ops::check_command_exists("rpm-ostree")?; + + let cache = UsersCache::new(); + + if cache.get_current_uid() != 0 { + bail!("You need to be root to rebase a local image! Try using 'sudo'."); + } + + clean_local_build_dir()?; + } + if self.push { ops::check_command_exists("cosign")?; ops::check_command_exists("skopeo")?; @@ -85,6 +118,7 @@ impl BuildCommand { self.build_image() } + /// Runs the command and exits if there is an error. pub fn run(&self) { trace!("BuildCommand::run()"); @@ -109,6 +143,10 @@ impl BuildCommand { info!("Build complete!"); + if self.rebase { + info!("Be sure to restart your computer to use your new changes!"); + } + Ok(()) } @@ -201,49 +239,56 @@ impl BuildCommand { info!("Generating full image name"); trace!("BuildCommand::generate_full_image_name({recipe:#?})"); - let image_name = match ( - env::var("CI_REGISTRY").ok(), - env::var("CI_PROJECT_NAMESPACE").ok(), - env::var("CI_PROJECT_NAME").ok(), - env::var("GITHUB_REPOSITORY_OWNER").ok(), - self.registry.as_ref(), - self.registry_path.as_ref(), - ) { - (_, _, _, _, Some(registry), Some(registry_path)) => { - trace!("registry={registry}, registry_path={registry_path}"); - format!( - "{}/{}/{}", - registry.trim().trim_matches('/'), - registry_path.trim().trim_matches('/'), - &recipe.name - ) - } - ( - Some(ci_registry), - Some(ci_project_namespace), - Some(ci_project_name), - None, - None, - None, - ) => { - trace!("CI_REGISTRY={ci_registry}, CI_PROJECT_NAMESPACE={ci_project_namespace}, CI_PROJECT_NAME={ci_project_name}"); - warn!("Generating Gitlab Registry image"); - format!( - "{ci_registry}/{ci_project_namespace}/{ci_project_name}/{}", - &recipe.name - ) - } - (None, None, None, Some(github_repository_owner), None, None) => { - trace!("GITHUB_REPOSITORY_OWNER={github_repository_owner}"); - warn!("Generating Github Registry image"); - format!("ghcr.io/{github_repository_owner}/{}", &recipe.name) - } - _ => { - trace!("Nothing to indicate an image name with a registry"); - if self.push { - bail!("Need '--registry' and '--registry-path' in order to push image"); + let image_name = if self.rebase { + let local_build_path = PathBuf::from(LOCAL_BUILD); + + let image_path = local_build_path.join(format!("{}.tar.gz", &recipe.name)); + format!("oci-archive:{}", image_path.display()) + } else { + match ( + env::var("CI_REGISTRY").ok(), + env::var("CI_PROJECT_NAMESPACE").ok(), + env::var("CI_PROJECT_NAME").ok(), + env::var("GITHUB_REPOSITORY_OWNER").ok(), + self.registry.as_ref(), + self.registry_path.as_ref(), + ) { + (_, _, _, _, Some(registry), Some(registry_path)) => { + trace!("registry={registry}, registry_path={registry_path}"); + format!( + "{}/{}/{}", + registry.trim().trim_matches('/'), + registry_path.trim().trim_matches('/'), + &recipe.name + ) + } + ( + Some(ci_registry), + Some(ci_project_namespace), + Some(ci_project_name), + None, + None, + None, + ) => { + trace!("CI_REGISTRY={ci_registry}, CI_PROJECT_NAMESPACE={ci_project_namespace}, CI_PROJECT_NAME={ci_project_name}"); + warn!("Generating Gitlab Registry image"); + format!( + "{ci_registry}/{ci_project_namespace}/{ci_project_name}/{}", + &recipe.name + ) + } + (None, None, None, Some(github_repository_owner), None, None) => { + trace!("GITHUB_REPOSITORY_OWNER={github_repository_owner}"); + warn!("Generating Github Registry image"); + format!("ghcr.io/{github_repository_owner}/{}", &recipe.name) + } + _ => { + trace!("Nothing to indicate an image name with a registry"); + if self.push { + bail!("Need '--registry' and '--registry-path' in order to push image"); + } + recipe.name.to_owned() } - recipe.name.to_owned() } }; @@ -261,7 +306,11 @@ impl BuildCommand { .next() .ok_or(anyhow!("We got here with no tags!?"))?; - let full_image = format!("{image_name}:{first_tag}"); + let full_image = if self.rebase { + image_name.to_owned() + } else { + format!("{image_name}:{first_tag}") + }; let status = match ( ops::check_command_exists("buildah"), @@ -293,7 +342,7 @@ impl BuildCommand { bail!("Failed to build {image_name}"); } - if tags.len() > 1 { + if tags.len() > 1 && !self.rebase { debug!("Tagging all images"); for tag in tags_iter { @@ -365,6 +414,19 @@ impl BuildCommand { } self.sign_images(image_name, first_tag)?; + } else if self.rebase { + debug!("Rebasing onto locally built image {image_name}"); + + if Command::new("rpm-ostree") + .arg("rebase") + .arg(format!("ostree-unverified-image:{full_image}")) + .status()? + .success() + { + info!("Successfully rebased to {full_image}"); + } else { + bail!("Failed to rebase to {full_image}"); + } } Ok(()) @@ -481,7 +543,7 @@ impl BuildCommand { } } -pub fn get_image_digest(image_name: &str, tag: &str) -> Result { +fn get_image_digest(image_name: &str, tag: &str) -> Result { trace!("get_image_digest({image_name}, {tag})"); let image_url = format!("docker://{image_name}:{tag}"); @@ -502,7 +564,7 @@ pub fn get_image_digest(image_name: &str, tag: &str) -> Result { )) } -pub fn check_cosign_files() -> Result<()> { +fn check_cosign_files() -> Result<()> { trace!("check_for_cosign_files()"); match ( @@ -547,3 +609,33 @@ pub fn check_cosign_files() -> Result<()> { } } } + +fn clean_local_build_dir() -> Result<()> { + trace!("clean_local_build_dir()"); + let local_build_path = Path::new(LOCAL_BUILD); + + if !local_build_path.exists() { + trace!( + "Creating build output dir at {}", + local_build_path.display() + ); + fs::create_dir_all(local_build_path)?; + } else { + debug!("Cleaning out build dir {LOCAL_BUILD}"); + + let entries = fs::read_dir(LOCAL_BUILD)?; + + for entry in entries { + let entry = entry?; + let path = entry.path(); + trace!("Found {}", path.display()); + + if path.is_file() && path.ends_with(".tar.gz") { + trace!("Removing {}", path.display()); + fs::remove_file(path)?; + } + } + } + + Ok(()) +} diff --git a/templates/Containerfile b/templates/Containerfile index c01035e..61545b7 100644 --- a/templates/Containerfile +++ b/templates/Containerfile @@ -21,6 +21,8 @@ COPY --from=docker.io/mikefarah/yq /usr/bin/yq /usr/bin/yq COPY --from=gcr.io/projectsigstore/cosign /ko-app/cosign /usr/bin/cosign +COPY --from=registry.gitlab.com/wunker-bunker/blue-build:installer /out/bb /usr/bin/bb + COPY config /tmp/config/ # Copy modules