diff --git a/Cargo.toml b/Cargo.toml index 9640505..a4bbbb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,9 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9.30" signal-hook = { version = "0.3.17", optional = true } -signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"], optional = true } +signal-hook-tokio = { version = "0.3.1", features = [ + "futures-v0_3", +], optional = true } shadow-rs = { version = "0.26" } sigstore = { version = "0.8.0", optional = true } tokio = { version = "1", features = ["full"], optional = true } @@ -48,7 +50,13 @@ which = "6" [features] default = [] nightly = ["builtin-podman"] -builtin-podman = ["podman-api", "tokio", "futures-util", "signal-hook-tokio", "signal-hook"] +builtin-podman = [ + "podman-api", + "tokio", + "futures-util", + "signal-hook-tokio", + "signal-hook", +] tls = ["podman-api/tls", "builtin-podman"] [dev-dependencies] diff --git a/src/commands/build.rs b/src/commands/build.rs index 70e754b..a571ef0 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -9,6 +9,7 @@ use std::{ use anyhow::{anyhow, bail, Result}; use clap::Args; +use colorized::{Color, Colors}; use log::{debug, info, trace, warn}; use typed_builder::TypedBuilder; use uuid::Uuid; @@ -34,7 +35,12 @@ use tokio::{ sync::oneshot::{self, Sender}, }; -use crate::{commands::template::TemplateCommand, constants::*, module_recipe::Recipe, ops}; +use crate::{ + commands::template::TemplateCommand, + constants::{self, *}, + module_recipe::Recipe, + ops, +}; use super::BlueBuildCommand; @@ -61,6 +67,15 @@ pub struct BuildCommand { #[builder(default)] push: bool, + /// Allow `bluebuild` to overwrite an existing + /// Containerfile without confirmation. + /// + /// This is not needed if the Containerfile is in + /// .gitignore or has already been built by `bluebuild`. + #[arg(short, long)] + #[builder(default)] + force: bool, + /// Archives the built image into a tarfile /// in the specified directory. #[arg(short, long)] @@ -146,8 +161,56 @@ impl BlueBuildCommand for BuildCommand { fn try_run(&mut self) -> Result<()> { trace!("BuildCommand::try_run()"); - let build_id = Uuid::new_v4(); + // Check if the Containerfile exists + // - If doesn't => *Build* + // - If it does: + // - check entry in .gitignore + // -> If it is => *Build* + // -> If isn't: + // - check if it has the BlueBuild tag (LABEL) + // -> If it does => *Ask* to add to .gitignore and remove from git + // -> If it doesn't => *Ask* to continue and override the file + let container_file_path = Path::new(constants::CONTAINER_FILE); + + if !self.force && container_file_path.exists() { + let gitignore = fs::read_to_string(constants::GITIGNORE_PATH)?; + + let is_ignored = gitignore + .lines() + .any(|line: &str| line.contains(constants::CONTAINER_FILE)); + + if !is_ignored { + let containerfile = fs::read_to_string(container_file_path)?; + let has_label = containerfile.lines().any(|line| { + let label = format!("LABEL {}", constants::BUILD_ID_LABEL); + line.to_string().trim().starts_with(&label) + }); + + let question = requestty::Question::confirm("build") + .message( + if has_label { + LABELED_ERROR_MESSAGE + } else { + NO_LABEL_ERROR_MESSAGE + } + .color(Colors::BrightYellowFg), + ) + .default(true) + .build(); + + if let Ok(answer) = requestty::prompt_one(question) { + if answer.as_bool().unwrap_or(false) { + ops::append_to_file( + constants::GITIGNORE_PATH, + &format!("/{}", constants::CONTAINER_FILE), + )?; + } + } + } + } + + let build_id = Uuid::new_v4(); if self.push && self.archive.is_some() { bail!("You cannot use '--archive' and '--push' at the same time"); } @@ -223,8 +286,7 @@ impl BuildCommand { image_name.to_string() } else { tags.first() - .map(|t| format!("{image_name}:{t}")) - .unwrap_or_else(|| image_name.to_string()) + .map_or_else(|| image_name.to_string(), |t| format!("{image_name}:{t}")) }; debug!("Full tag is {first_image_name}"); @@ -473,8 +535,7 @@ impl BuildCommand { image_name.to_string() } else { tags.first() - .map(|t| format!("{image_name}:{t}")) - .unwrap_or_else(|| image_name.to_string()) + .map_or_else(|| image_name.to_string(), |t| format!("{image_name}:{t}")) }; info!("Building image {full_image}"); @@ -575,9 +636,7 @@ fn sign_images(image_name: &str, tag: Option<&str>) -> Result<()> { env::set_var("COSIGN_YES", "true"); let image_digest = get_image_digest(image_name, tag)?; - let image_name_tag = tag - .map(|t| format!("{image_name}:{t}")) - .unwrap_or_else(|| image_name.to_owned()); + let image_name_tag = tag.map_or_else(|| image_name.to_owned(), |t| format!("{image_name}:{t}")); match ( env::var(CI_DEFAULT_BRANCH), diff --git a/src/commands/local.rs b/src/commands/local.rs index a4ad5ca..b019afd 100644 --- a/src/commands/local.rs +++ b/src/commands/local.rs @@ -30,6 +30,15 @@ pub struct LocalCommonArgs { #[arg(short, long)] #[builder(default)] reboot: bool, + + /// Allow `bluebuild` to overwrite an existing + /// Containerfile without confirmation. + /// + /// This is not needed if the Containerfile is in + /// .gitignore or has already been built by `bluebuild`. + #[arg(short, long)] + #[builder(default)] + force: bool, } #[derive(Default, Clone, Debug, TypedBuilder, Args)] @@ -49,6 +58,7 @@ impl BlueBuildCommand for UpgradeCommand { let mut build = BuildCommand::builder() .recipe(self.common.recipe.clone()) .archive(LOCAL_BUILD) + .force(self.common.force) .build(); let image_name = build.generate_full_image_name(&recipe)?; @@ -96,6 +106,7 @@ impl BlueBuildCommand for RebaseCommand { let mut build = BuildCommand::builder() .recipe(self.common.recipe.clone()) .archive(LOCAL_BUILD) + .force(self.common.force) .build(); let image_name = build.generate_full_image_name(&recipe)?; diff --git a/src/constants.rs b/src/constants.rs index f036a44..bebf4ec 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,7 +1,9 @@ // Paths pub const ARCHIVE_SUFFIX: &str = "tar.gz"; pub const COSIGN_PATH: &str = "./cosign.pub"; +pub const GITIGNORE_PATH: &str = ".gitignore"; pub const LOCAL_BUILD: &str = "/etc/bluebuild"; +pub const CONTAINER_FILE: &str = "Containerfile"; pub const MODULES_PATH: &str = "./config/modules"; pub const RECIPE_PATH: &str = "./config/recipe.yml"; pub const RUN_PODMAN_SOCK: &str = "/run/podman/podman.sock"; @@ -54,3 +56,13 @@ pub const UNKNOWN_SHELL: &str = ""; pub const UNKNOWN_VERSION: &str = ""; pub const UNKNOWN_TERMINAL: &str = ""; pub const GITHUB_CHAR_LIMIT: usize = 8100; // Magic number accepted by Github + +// Messages +pub const LABELED_ERROR_MESSAGE: &str = + "It looks you have a BlueBuild-generated Containerfile that is not .gitignored. \ +Do you want to remove it from git and add it to .gitignore? (This will still continue the build)"; + +pub const NO_LABEL_ERROR_MESSAGE: &str = + "It looks you have a Containerfile that has not been generated by BlueBuild. \ +Running `build` will override your Containerfile and add an entry to the .gitignore. \ +Do you want to continue?"; diff --git a/src/ops.rs b/src/ops.rs index 6393414..c4e5400 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -1,7 +1,8 @@ +use std::{io::Write, process::Command}; + use anyhow::{anyhow, Result}; use format_serde_error::SerdeError; use log::{debug, trace}; -use std::process::Command; pub fn check_command_exists(command: &str) -> Result<()> { trace!("check_command_exists({command})"); @@ -23,6 +24,19 @@ pub fn check_command_exists(command: &str) -> Result<()> { } } +pub fn append_to_file(file_path: &str, content: &str) -> Result<()> { + trace!("append_to_file({file_path}, {content})"); + debug!("Appending {content} to {file_path}"); + + let mut file = std::fs::OpenOptions::new() + .append(true) + .create(true) + .open(file_path)?; + + writeln!(file, "\n{content}")?; + Ok(()) +} + pub fn serde_yaml_err(contents: &str) -> impl Fn(serde_yaml::Error) -> SerdeError + '_ { |err: serde_yaml::Error| { let location = err.location();