refactor: Create SigningDriver and CiDriver (#197)
This also includes a new `login` command. The signing and CI logic is now using the Driver trait system along with a new experimental sigstore signing driver. New static macros have also been created to make implementation management easier for `Command` usage and `Driver` trait implementation calls. --------- Co-authored-by: xyny <60004820+xynydev@users.noreply.github.com>
This commit is contained in:
parent
3ecb0d3d93
commit
8ce83ba7ff
63 changed files with 6468 additions and 2083 deletions
|
|
@ -9,38 +9,30 @@ license.workspace = true
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
atty = "0.2"
|
||||
base64 = "0.22.1"
|
||||
blake2 = "0.10.6"
|
||||
base64 = "0.22"
|
||||
blake2 = "0.10"
|
||||
directories = "5"
|
||||
rand = "0.8.5"
|
||||
log4rs = { version = "1.3.0", features = ["background_rotation"] }
|
||||
nix = { version = "0.29.0", features = ["signal"] }
|
||||
nu-ansi-term = { version = "0.50.0", features = ["gnu_legacy"] }
|
||||
os_pipe = { version = "1", features = ["io_safety"] }
|
||||
docker_credential = "1"
|
||||
format_serde_error = "0.3"
|
||||
process_control = { version = "4", features = ["crossbeam-channel"] }
|
||||
signal-hook = { version = "0.3.17", features = ["extended-siginfo"] }
|
||||
syntect = "5"
|
||||
which = "6"
|
||||
|
||||
chrono.workspace = true
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
colored.workspace = true
|
||||
format_serde_error.workspace = true
|
||||
indicatif.workspace = true
|
||||
indicatif-log-bridge.workspace = true
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
log.workspace = true
|
||||
miette.workspace = true
|
||||
once_cell.workspace = true
|
||||
tempdir.workspace = true
|
||||
serde.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
typed-builder.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
syntect = "5.2.0"
|
||||
syntect = "5"
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub const IMAGE_VERSION_LABEL: &str = "org.opencontainers.image.version";
|
|||
// BlueBuild vars
|
||||
pub const BB_BUILDKIT_CACHE_GHA: &str = "BB_BUILDKIT_CACHE_GHA";
|
||||
pub const BB_PASSWORD: &str = "BB_PASSWORD";
|
||||
pub const BB_PRIVATE_KEY: &str = "BB_PRIVATE_KEY";
|
||||
pub const BB_REGISTRY: &str = "BB_REGISTRY";
|
||||
pub const BB_REGISTRY_NAMESPACE: &str = "BB_REGISTRY_NAMESPACE";
|
||||
pub const BB_USERNAME: &str = "BB_USERNAME";
|
||||
|
|
@ -27,7 +28,9 @@ pub const BB_USERNAME: &str = "BB_USERNAME";
|
|||
pub const DOCKER_HOST: &str = "DOCKER_HOST";
|
||||
|
||||
// Cosign vars
|
||||
pub const COSIGN_PASSWORD: &str = "COSIGN_PASSWORD";
|
||||
pub const COSIGN_PRIVATE_KEY: &str = "COSIGN_PRIVATE_KEY";
|
||||
pub const COSIGN_YES: &str = "COSIGN_YES";
|
||||
pub const GITHUB_TOKEN_ISSUER_URL: &str = "https://token.actions.githubusercontent.com";
|
||||
pub const SIGSTORE_ID_TOKEN: &str = "SIGSTORE_ID_TOKEN";
|
||||
|
||||
|
|
@ -35,6 +38,7 @@ pub const SIGSTORE_ID_TOKEN: &str = "SIGSTORE_ID_TOKEN";
|
|||
pub const GITHUB_ACTIONS: &str = "GITHUB_ACTIONS";
|
||||
pub const GITHUB_ACTOR: &str = "GITHUB_ACTOR";
|
||||
pub const GITHUB_EVENT_NAME: &str = "GITHUB_EVENT_NAME";
|
||||
pub const GITHUB_EVENT_PATH: &str = "GITHUB_EVENT_PATH";
|
||||
pub const GITHUB_REF_NAME: &str = "GITHUB_REF_NAME";
|
||||
pub const GITHUB_RESPOSITORY: &str = "GITHUB_REPOSITORY";
|
||||
pub const GITHUB_REPOSITORY_OWNER: &str = "GITHUB_REPOSITORY_OWNER";
|
||||
|
|
@ -58,6 +62,7 @@ pub const CI_SERVER_PROTOCOL: &str = "CI_SERVER_PROTOCOL";
|
|||
pub const CI_REGISTRY: &str = "CI_REGISTRY";
|
||||
pub const CI_REGISTRY_PASSWORD: &str = "CI_REGISTRY_PASSWORD";
|
||||
pub const CI_REGISTRY_USER: &str = "CI_REGISTRY_USER";
|
||||
pub const GITLAB_CI: &str = "GITLAB_CI";
|
||||
|
||||
// Terminal vars
|
||||
pub const TERM_PROGRAM: &str = "TERM_PROGRAM";
|
||||
|
|
@ -67,10 +72,12 @@ pub const LC_TERMINAL_VERSION: &str = "LC_TERMINAL_VERSION";
|
|||
pub const XDG_RUNTIME_DIR: &str = "XDG_RUNTIME_DIR";
|
||||
|
||||
// Misc
|
||||
pub const COSIGN_IMAGE: &str = "gcr.io/projectsigstore/cosign:latest";
|
||||
pub const OCI_ARCHIVE: &str = "oci-archive";
|
||||
pub const OSTREE_IMAGE_SIGNED: &str = "ostree-image-signed";
|
||||
pub const OSTREE_UNVERIFIED_IMAGE: &str = "ostree-unverified-image";
|
||||
pub const SKOPEO_IMAGE: &str = "quay.io/skopeo/stable:latest";
|
||||
pub const TEMPLATE_REPO_URL: &str = "https://github.com/blue-build/template.git";
|
||||
pub const UNKNOWN_SHELL: &str = "<unknown shell>";
|
||||
pub const UNKNOWN_VERSION: &str = "<unknown version>";
|
||||
pub const UNKNOWN_TERMINAL: &str = "<unknown terminal>";
|
||||
|
|
|
|||
177
utils/src/credentials.rs
Normal file
177
utils/src/credentials.rs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
use std::{
|
||||
env,
|
||||
sync::{LazyLock, Mutex},
|
||||
};
|
||||
|
||||
use clap::Args;
|
||||
use docker_credential::DockerCredential;
|
||||
use log::trace;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use crate::{
|
||||
constants::{
|
||||
BB_PASSWORD, BB_REGISTRY, BB_USERNAME, CI_REGISTRY, CI_REGISTRY_PASSWORD, CI_REGISTRY_USER,
|
||||
GITHUB_ACTIONS, GITHUB_ACTOR, GITHUB_TOKEN,
|
||||
},
|
||||
string,
|
||||
};
|
||||
|
||||
static INIT: LazyLock<Mutex<bool>> = LazyLock::new(|| Mutex::new(false));
|
||||
|
||||
/// Stored user creds.
|
||||
///
|
||||
/// This is a special handoff static ref that is consumed
|
||||
/// by the `ENV_CREDENTIALS` static ref. This can be set
|
||||
/// at the beginning of a command for future calls for
|
||||
/// creds to source from.
|
||||
static INIT_CREDS: Mutex<CredentialsArgs> = Mutex::new(CredentialsArgs {
|
||||
username: None,
|
||||
password: None,
|
||||
registry: None,
|
||||
});
|
||||
|
||||
/// Stores the global env credentials.
|
||||
///
|
||||
/// This on load will determine the credentials based off of
|
||||
/// `USER_CREDS` and env vars from CI systems. Once this is called
|
||||
/// the value is stored and cannot change.
|
||||
///
|
||||
/// If you have user
|
||||
/// provided credentials, make sure you update `USER_CREDS`
|
||||
/// before trying to access this reference.
|
||||
static ENV_CREDENTIALS: LazyLock<Option<Credentials>> = LazyLock::new(|| {
|
||||
let (username, password, registry) = {
|
||||
INIT_CREDS.lock().map_or((None, None, None), |mut creds| {
|
||||
(
|
||||
creds.username.take(),
|
||||
creds.password.take(),
|
||||
creds.registry.take(),
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
let registry = match (
|
||||
registry,
|
||||
env::var(CI_REGISTRY).ok(),
|
||||
env::var(GITHUB_ACTIONS).ok(),
|
||||
) {
|
||||
(Some(registry), _, _) if !registry.is_empty() => registry,
|
||||
(None, Some(ci_registry), None) if !ci_registry.is_empty() => ci_registry,
|
||||
(None, None, Some(_)) => string!("ghcr.io"),
|
||||
_ => return None,
|
||||
};
|
||||
trace!("Registry: {registry:?}");
|
||||
|
||||
let docker_creds = docker_credential::get_credential(®istry).ok();
|
||||
let podman_creds = docker_credential::get_podman_credential(®istry).ok();
|
||||
|
||||
let username = match (
|
||||
username,
|
||||
env::var(CI_REGISTRY_USER).ok(),
|
||||
env::var(GITHUB_ACTOR).ok(),
|
||||
&docker_creds,
|
||||
&podman_creds,
|
||||
) {
|
||||
(Some(username), _, _, _, _) if !username.is_empty() => username,
|
||||
(_, _, _, Some(DockerCredential::UsernamePassword(username, _)), _)
|
||||
| (_, _, _, _, Some(DockerCredential::UsernamePassword(username, _)))
|
||||
if !username.is_empty() =>
|
||||
{
|
||||
username.clone()
|
||||
}
|
||||
(None, Some(ci_registry_user), None, _, _) if !ci_registry_user.is_empty() => {
|
||||
ci_registry_user
|
||||
}
|
||||
(None, None, Some(github_actor), _, _) if !github_actor.is_empty() => github_actor,
|
||||
_ => return None,
|
||||
};
|
||||
trace!("Username: {username:?}");
|
||||
|
||||
let password = match (
|
||||
password,
|
||||
env::var(CI_REGISTRY_PASSWORD).ok(),
|
||||
env::var(GITHUB_TOKEN).ok(),
|
||||
&docker_creds,
|
||||
&podman_creds,
|
||||
) {
|
||||
(Some(password), _, _, _, _) if !password.is_empty() => password,
|
||||
(_, _, _, Some(DockerCredential::UsernamePassword(_, password)), _)
|
||||
| (_, _, _, _, Some(DockerCredential::UsernamePassword(_, password)))
|
||||
if !password.is_empty() =>
|
||||
{
|
||||
password.clone()
|
||||
}
|
||||
(None, Some(ci_registry_password), None, _, _) if !ci_registry_password.is_empty() => {
|
||||
ci_registry_password
|
||||
}
|
||||
(None, None, Some(registry_token), _, _) if !registry_token.is_empty() => registry_token,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(
|
||||
Credentials::builder()
|
||||
.registry(registry)
|
||||
.username(username)
|
||||
.password(password)
|
||||
.build(),
|
||||
)
|
||||
});
|
||||
|
||||
/// The credentials for logging into image registries.
|
||||
#[derive(Debug, Default, Clone, TypedBuilder)]
|
||||
pub struct Credentials {
|
||||
pub registry: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl Credentials {
|
||||
/// Set the users credentials for
|
||||
/// the current set of actions.
|
||||
///
|
||||
/// Be sure to call this before trying to use
|
||||
/// any strategy that requires credentials as
|
||||
/// the environment credentials are lazy allocated.
|
||||
///
|
||||
/// # Panics
|
||||
/// Will panic if it can't lock the mutex.
|
||||
pub fn init(args: CredentialsArgs) {
|
||||
trace!("Credentials::init()");
|
||||
let mut initialized = INIT.lock().expect("Must lock INIT");
|
||||
|
||||
if !*initialized {
|
||||
let mut creds_lock = INIT_CREDS.lock().expect("Must lock USER_CREDS");
|
||||
*creds_lock = args;
|
||||
drop(creds_lock);
|
||||
let _ = ENV_CREDENTIALS.as_ref();
|
||||
|
||||
*initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the credentials for the current set of actions.
|
||||
pub fn get() -> Option<&'static Self> {
|
||||
trace!("credentials::get()");
|
||||
ENV_CREDENTIALS.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, TypedBuilder, Args)]
|
||||
pub struct CredentialsArgs {
|
||||
/// The registry's domain name.
|
||||
#[arg(long, env = BB_REGISTRY)]
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub registry: Option<String>,
|
||||
|
||||
/// The username to login to the
|
||||
/// container registry.
|
||||
#[arg(short = 'U', long, env = BB_USERNAME, hide_env_values = true)]
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub username: Option<String>,
|
||||
|
||||
/// The password to login to the
|
||||
/// container registry.
|
||||
#[arg(short = 'P', long, env = BB_PASSWORD, hide_env_values = true)]
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
pub mod command_output;
|
||||
pub mod constants;
|
||||
pub mod logging;
|
||||
pub mod signal_handler;
|
||||
pub mod credentials;
|
||||
mod macros;
|
||||
pub mod syntax_highlighting;
|
||||
|
||||
use std::{
|
||||
os::unix::ffi::OsStrExt,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
|
@ -17,9 +16,10 @@ use blake2::{
|
|||
digest::{Update, VariableOutput},
|
||||
Blake2bVar,
|
||||
};
|
||||
use chrono::Local;
|
||||
use format_serde_error::SerdeError;
|
||||
use log::trace;
|
||||
use miette::{miette, IntoDiagnostic, Result};
|
||||
use miette::{miette, Context, IntoDiagnostic, Result};
|
||||
|
||||
use crate::constants::CONTAINER_FILE;
|
||||
|
||||
|
|
@ -33,8 +33,7 @@ pub fn check_command_exists(command: &str) -> Result<()> {
|
|||
trace!("check_command_exists({command})");
|
||||
|
||||
trace!("which {command}");
|
||||
if Command::new("which")
|
||||
.arg(command)
|
||||
if cmd!("which", command)
|
||||
.output()
|
||||
.into_diagnostic()?
|
||||
.status
|
||||
|
|
@ -109,3 +108,18 @@ pub fn generate_containerfile_path<T: AsRef<Path>>(path: T) -> Result<PathBuf> {
|
|||
BASE64_URL_SAFE_NO_PAD.encode(buf)
|
||||
)))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_tag_timestamp() -> String {
|
||||
Local::now().format("%Y%m%d").to_string()
|
||||
}
|
||||
|
||||
/// Get's the env var wrapping it with a miette error
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the env var doesn't exist.
|
||||
pub fn get_env_var(key: &str) -> Result<String> {
|
||||
std::env::var(key)
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to get {key}'"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,414 +0,0 @@
|
|||
use std::{
|
||||
env,
|
||||
fs::OpenOptions,
|
||||
io::{BufRead, BufReader, Result, Write as IoWrite},
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, ExitStatus, Stdio},
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use chrono::Local;
|
||||
use colored::{control::ShouldColorize, ColoredString, Colorize};
|
||||
use indicatif::{MultiProgress, ProgressBar};
|
||||
use indicatif_log_bridge::LogWrapper;
|
||||
use log::{warn, Level, LevelFilter, Record};
|
||||
use log4rs::{
|
||||
append::{
|
||||
console::ConsoleAppender,
|
||||
rolling_file::{
|
||||
policy::compound::{
|
||||
roll::fixed_window::FixedWindowRoller, trigger::size::SizeTrigger, CompoundPolicy,
|
||||
},
|
||||
RollingFileAppender,
|
||||
},
|
||||
},
|
||||
config::{Appender, Root},
|
||||
encode::{pattern::PatternEncoder, Encode, Write},
|
||||
Config, Logger as L4RSLogger,
|
||||
};
|
||||
use nu_ansi_term::Color;
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::Rng;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use crate::signal_handler::{add_pid, remove_pid};
|
||||
|
||||
static MULTI_PROGRESS: Lazy<MultiProgress> = Lazy::new(MultiProgress::new);
|
||||
static LOG_DIR: Lazy<Mutex<PathBuf>> = Lazy::new(|| Mutex::new(PathBuf::new()));
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Logger {
|
||||
modules: Vec<(String, LevelFilter)>,
|
||||
level: LevelFilter,
|
||||
log_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Logger {
|
||||
const TRIGGER_FILE_SIZE: u64 = 10 * 1024;
|
||||
const ARCHIVE_FILENAME_PATTERN: &'static str = "bluebuild-log.{}.log";
|
||||
const LOG_FILENAME: &'static str = "bluebuild-log.log";
|
||||
const LOG_FILE_COUNT: u32 = 4;
|
||||
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn filter_modules<I, S>(&mut self, filter_modules: I) -> &mut Self
|
||||
where
|
||||
I: IntoIterator<Item = (S, LevelFilter)>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
self.modules = filter_modules
|
||||
.into_iter()
|
||||
.map(|(module, level)| (module.as_ref().to_string(), level))
|
||||
.collect::<Vec<_>>();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn filter_level(&mut self, filter_level: LevelFilter) -> &mut Self {
|
||||
self.level = filter_level;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn log_out_dir<P>(&mut self, path: Option<P>) -> &mut Self
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
self.log_dir = path.map(|p| p.as_ref().to_path_buf());
|
||||
self
|
||||
}
|
||||
|
||||
/// Initializes logging for the application.
|
||||
///
|
||||
/// # Panics
|
||||
/// Will panic if logging is unable to be initialized.
|
||||
pub fn init(&self) {
|
||||
let home = env::var("HOME").expect("$HOME should be defined");
|
||||
let log_dir = self.log_dir.as_ref().map_or_else(
|
||||
|| Path::new(home.as_str()).join(".local/share/bluebuild"),
|
||||
Clone::clone,
|
||||
);
|
||||
|
||||
let mut lock = LOG_DIR.lock().expect("Should lock LOG_DIR");
|
||||
lock.clone_from(&log_dir);
|
||||
drop(lock);
|
||||
|
||||
let log_out_path = log_dir.join(Self::LOG_FILENAME);
|
||||
let log_archive_pattern =
|
||||
format!("{}/{}", log_dir.display(), Self::ARCHIVE_FILENAME_PATTERN);
|
||||
|
||||
let stderr = ConsoleAppender::builder()
|
||||
.encoder(Box::new(
|
||||
CustomPatternEncoder::builder()
|
||||
.filter_modules(self.modules.clone())
|
||||
.build(),
|
||||
))
|
||||
.target(log4rs::append::console::Target::Stderr)
|
||||
.tty_only(true)
|
||||
.build();
|
||||
|
||||
let file = RollingFileAppender::builder()
|
||||
.encoder(Box::new(PatternEncoder::new("{d} - {l} - {m}{n}")))
|
||||
.build(
|
||||
log_out_path,
|
||||
Box::new(CompoundPolicy::new(
|
||||
Box::new(SizeTrigger::new(Self::TRIGGER_FILE_SIZE)),
|
||||
Box::new(
|
||||
FixedWindowRoller::builder()
|
||||
.build(&log_archive_pattern, Self::LOG_FILE_COUNT)
|
||||
.expect("Roller should be created"),
|
||||
),
|
||||
)),
|
||||
)
|
||||
.expect("Must be able to create log FileAppender");
|
||||
|
||||
let config = Config::builder()
|
||||
.appender(Appender::builder().build("stderr", Box::new(stderr)))
|
||||
.appender(Appender::builder().build("file", Box::new(file)))
|
||||
.build(
|
||||
Root::builder()
|
||||
.appender("stderr")
|
||||
.appender("file")
|
||||
.build(self.level),
|
||||
)
|
||||
.expect("Logger config should build");
|
||||
|
||||
let logger = L4RSLogger::new(config);
|
||||
|
||||
LogWrapper::new(MULTI_PROGRESS.clone(), logger)
|
||||
.try_init()
|
||||
.expect("LogWrapper should initialize");
|
||||
}
|
||||
|
||||
pub fn multi_progress() -> MultiProgress {
|
||||
MULTI_PROGRESS.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Logger {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
modules: vec![],
|
||||
level: LevelFilter::Info,
|
||||
log_dir: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait ColoredLevel {
|
||||
fn colored(&self) -> ColoredString;
|
||||
}
|
||||
|
||||
impl ColoredLevel for Level {
|
||||
fn colored(&self) -> ColoredString {
|
||||
match self {
|
||||
Self::Error => Self::Error.as_str().red(),
|
||||
Self::Warn => Self::Warn.as_str().yellow(),
|
||||
Self::Info => Self::Info.as_str().green(),
|
||||
Self::Debug => Self::Debug.as_str().blue(),
|
||||
Self::Trace => Self::Trace.as_str().cyan(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CommandLogging {
|
||||
/// Prints each line of stdout/stderr with an image ref string
|
||||
/// and a progress spinner. This helps to keep track of every
|
||||
/// build running in parallel.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if there was an issue executing the process.
|
||||
fn status_image_ref_progress<T, U>(self, image_ref: T, message: U) -> Result<ExitStatus>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
U: AsRef<str>;
|
||||
}
|
||||
|
||||
impl CommandLogging for Command {
|
||||
fn status_image_ref_progress<T, U>(mut self, image_ref: T, message: U) -> Result<ExitStatus>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
U: AsRef<str>,
|
||||
{
|
||||
let ansi_color = gen_random_ansi_color();
|
||||
let name = color_str(&image_ref, ansi_color);
|
||||
let short_name = color_str(shorten_name(&image_ref), ansi_color);
|
||||
let log_prefix = Arc::new(log_header(short_name));
|
||||
let (reader, writer) = os_pipe::pipe()?;
|
||||
|
||||
self.stdout(writer.try_clone()?)
|
||||
.stderr(writer)
|
||||
.stdin(Stdio::piped());
|
||||
|
||||
let progress = Logger::multi_progress()
|
||||
.add(ProgressBar::new_spinner().with_message(format!("{} {name}", message.as_ref())));
|
||||
progress.enable_steady_tick(Duration::from_millis(100));
|
||||
|
||||
let mut child = self.spawn()?;
|
||||
|
||||
let child_pid = child.id();
|
||||
add_pid(child_pid);
|
||||
|
||||
// We drop the `Command` to prevent blocking on writer
|
||||
// https://docs.rs/os_pipe/latest/os_pipe/#examples
|
||||
drop(self);
|
||||
|
||||
let reader = BufReader::new(reader);
|
||||
let log_file_path = {
|
||||
let lock = LOG_DIR.lock().expect("Should lock LOG_DIR");
|
||||
lock.join(format!(
|
||||
"{}.log",
|
||||
image_ref.as_ref().replace(['/', ':', '.'], "_")
|
||||
))
|
||||
};
|
||||
let log_file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(log_file_path.as_path())?;
|
||||
|
||||
thread::spawn(move || {
|
||||
let mp = Logger::multi_progress();
|
||||
reader.lines().for_each(|line| {
|
||||
if let Ok(l) = line {
|
||||
let text = format!("{log_prefix} {l}");
|
||||
if mp.is_hidden() {
|
||||
eprintln!("{text}");
|
||||
} else {
|
||||
mp.println(text).unwrap();
|
||||
}
|
||||
if let Err(e) = writeln!(&log_file, "{l}") {
|
||||
warn!(
|
||||
"Failed to write to log for build {}: {e:?}",
|
||||
log_file_path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let status = child.wait()?;
|
||||
remove_pid(child_pid);
|
||||
|
||||
progress.finish();
|
||||
Logger::multi_progress().remove(&progress);
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, TypedBuilder)]
|
||||
struct CustomPatternEncoder {
|
||||
#[builder(default, setter(into))]
|
||||
filter_modules: Vec<(String, LevelFilter)>,
|
||||
}
|
||||
|
||||
impl Encode for CustomPatternEncoder {
|
||||
fn encode(&self, w: &mut dyn Write, record: &Record) -> anyhow::Result<()> {
|
||||
if record.module_path().is_some_and(|mp| {
|
||||
self.filter_modules
|
||||
.iter()
|
||||
.any(|(module, level)| mp.contains(module) && *level <= record.level())
|
||||
}) {
|
||||
Ok(())
|
||||
} else {
|
||||
match log::max_level() {
|
||||
LevelFilter::Error | LevelFilter::Warn | LevelFilter::Info => Ok(writeln!(
|
||||
w,
|
||||
"{prefix} {args}",
|
||||
prefix = log_header(format!(
|
||||
"{level:width$}",
|
||||
level = record.level().colored(),
|
||||
width = 5,
|
||||
)),
|
||||
args = record.args(),
|
||||
)?),
|
||||
LevelFilter::Debug => Ok(writeln!(
|
||||
w,
|
||||
"{prefix} {args}",
|
||||
prefix = log_header(format!(
|
||||
"{level:>width$}",
|
||||
level = record.level().colored(),
|
||||
width = 5,
|
||||
)),
|
||||
args = record.args(),
|
||||
)?),
|
||||
LevelFilter::Trace => Ok(writeln!(
|
||||
w,
|
||||
"{prefix} {args}",
|
||||
prefix = log_header(format!(
|
||||
"{level:width$} {module}:{line}",
|
||||
level = record.level().colored(),
|
||||
width = 5,
|
||||
module = record
|
||||
.module_path()
|
||||
.map_or_else(|| "", |p| p)
|
||||
.bright_yellow(),
|
||||
line = record
|
||||
.line()
|
||||
.map_or_else(String::new, |l| l.to_string())
|
||||
.bright_green(),
|
||||
)),
|
||||
args = record.args(),
|
||||
)?),
|
||||
LevelFilter::Off => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to keep the style of logs consistent between
|
||||
/// normal log use and command output.
|
||||
fn log_header<T: AsRef<str>>(text: T) -> String {
|
||||
let text = text.as_ref();
|
||||
match log::max_level() {
|
||||
LevelFilter::Error | LevelFilter::Warn | LevelFilter::Info => {
|
||||
format!("{text} {sep}", sep = "=>".bold())
|
||||
}
|
||||
LevelFilter::Debug | LevelFilter::Trace => format!(
|
||||
"[{time} {text}] {sep}",
|
||||
time = Local::now().format("%H:%M:%S"),
|
||||
sep = "=>".bold(),
|
||||
),
|
||||
LevelFilter::Off => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shortens the image name so that it won't take up the
|
||||
/// entire width of the terminal. This is a similar format
|
||||
/// to what Earthly does in their terminal output for long
|
||||
/// images on their log prefix output.
|
||||
///
|
||||
/// # Examples
|
||||
/// `ghcr.io/blue-build/cli:latest` -> `g.i/b/cli:latest`
|
||||
/// `registry.gitlab.com/some/namespace/image:latest` -> `r.g.c/s/n/image:latest`
|
||||
#[must_use]
|
||||
fn shorten_name<T>(text: T) -> String
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
let text = text.as_ref();
|
||||
|
||||
// Split the reference by colon to separate the tag or digest
|
||||
let mut parts = text.split(':');
|
||||
|
||||
let path = match parts.next() {
|
||||
None => return text.to_string(),
|
||||
Some(path) => path,
|
||||
};
|
||||
let tag = parts.next();
|
||||
|
||||
// Split the path by slash to work on each part
|
||||
let path_parts: Vec<&str> = path.split('/').collect();
|
||||
|
||||
// Shorten each part except the last one to their initial letters
|
||||
let shortened_parts: Vec<String> = path_parts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, part)| {
|
||||
if i < path_parts.len() - 1 {
|
||||
// Split on '.' and shorten each section
|
||||
part.split('.')
|
||||
.filter_map(|p| p.chars().next())
|
||||
.map(|c| c.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(".")
|
||||
} else {
|
||||
(*part).into() // Keep the last part as it is
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Rejoin the parts with '/'
|
||||
let joined_path = shortened_parts.join("/");
|
||||
|
||||
// If there was a tag, append it back with ':', otherwise just return the path
|
||||
match tag {
|
||||
Some(t) => format!("{joined_path}:{t}"),
|
||||
None => joined_path,
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_random_ansi_color() -> u8 {
|
||||
// ANSI extended color range
|
||||
// https://www.ditig.com/publications/256-colors-cheat-sheet
|
||||
const LOW_END: u8 = 21; // Blue1 #0000ff rgb(0,0,255) hsl(240,100%,50%)
|
||||
const HIGH_END: u8 = 230; // Cornsilk1 #ffffd7 rgb(255,255,215) hsl(60,100%,92%)
|
||||
|
||||
rand::thread_rng().gen_range(LOW_END..=HIGH_END)
|
||||
}
|
||||
|
||||
fn color_str<T>(text: T, ansi_color: u8) -> String
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
if ShouldColorize::from_env().should_colorize() {
|
||||
Color::Fixed(ansi_color)
|
||||
.paint(text.as_ref().to_string())
|
||||
.to_string()
|
||||
} else {
|
||||
text.as_ref().to_string()
|
||||
}
|
||||
}
|
||||
150
utils/src/macros.rs
Normal file
150
utils/src/macros.rs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
/// Creates or modifies a `std::process::Command` adding args.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use blue_build_utils::cmd;
|
||||
///
|
||||
/// const NAME: &str = "Bob";
|
||||
/// let mut command = cmd!("echo", "Hello world!");
|
||||
/// cmd!(command, "This is Joe.", format!("And this is {NAME}"));
|
||||
/// command.status().unwrap();
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! cmd {
|
||||
($command:expr) => {
|
||||
::std::process::Command::new($command)
|
||||
};
|
||||
($command:ident, $($tail:tt)*) => {
|
||||
cmd!(@ $command, $($tail)*)
|
||||
};
|
||||
($command:expr, $($tail:tt)*) => {
|
||||
{
|
||||
let mut c = cmd!($command);
|
||||
cmd!(@ c, $($tail)*);
|
||||
c
|
||||
}
|
||||
};
|
||||
(@ $command:ident $(,)?) => { };
|
||||
(@ $command:ident, for $for_expr:expr $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
for arg in $for_expr.iter() {
|
||||
cmd!($command, arg);
|
||||
}
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, for $iter:ident in $for_expr:expr => [ $($arg:expr),* $(,)? ] $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
for $iter in $for_expr.iter() {
|
||||
$(cmd!(@ $command, $arg);)*
|
||||
}
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, for $iter:ident in $for_expr:expr => $arg:expr $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
for $iter in $for_expr.iter() {
|
||||
cmd!(@ $command, $arg);
|
||||
}
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, if let $let_pat:pat = $if_expr:expr => [ $($arg:expr),* $(,)? ] $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
if let $let_pat = $if_expr {
|
||||
$(cmd!(@ $command, $arg);)*
|
||||
}
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, if let $let_pat:pat = $if_expr:expr => $arg:expr $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
if let $let_pat = $if_expr {
|
||||
cmd!(@ $command, $arg);
|
||||
}
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, if $if_expr:expr => [ $($arg:expr),* $(,)?] $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
if $if_expr {
|
||||
$(cmd!(@ $command, $arg);)*
|
||||
}
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, if $if_expr:expr => $arg:expr $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
if $if_expr {
|
||||
cmd!(@ $command, $arg);
|
||||
}
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, |$cmd_ref:ident|? $op:block $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
let op_fn = |$cmd_ref: &mut ::std::process::Command| -> Result<()> {
|
||||
$op
|
||||
Ok(())
|
||||
};
|
||||
op_fn(&mut $command)?;
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, |$cmd_ref:ident| $op:block $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
let op_fn = |$cmd_ref: &mut ::std::process::Command| $op;
|
||||
op_fn(&mut $command);
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, $key:expr => $value:expr $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
$command.env($key, $value);
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, stdin = $pipe:expr $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
$command.stdin($pipe);
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, stdout = $pipe:expr $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
$command.stdout($pipe);
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, stderr = $pipe:expr $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
$command.stderr($pipe);
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
(@ $command:ident, $arg:expr $(, $($tail:tt)*)?) => {
|
||||
{
|
||||
$command.arg($arg);
|
||||
$(cmd!(@ $command, $($tail)*);)*
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! string {
|
||||
($str:expr) => {
|
||||
String::from($str)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! string_vec {
|
||||
($($string:expr),+ $(,)?) => {
|
||||
{
|
||||
use $crate::string;
|
||||
vec![
|
||||
$(string!($string),)*
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
use std::{
|
||||
fs,
|
||||
path::PathBuf,
|
||||
process::{self, Command},
|
||||
sync::{atomic::AtomicBool, Arc, Mutex},
|
||||
thread,
|
||||
};
|
||||
|
||||
use log::{debug, error, trace, warn};
|
||||
use nix::{
|
||||
libc::{SIGABRT, SIGCONT, SIGHUP, SIGTSTP},
|
||||
sys::signal::{kill, Signal},
|
||||
unistd::Pid,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use signal_hook::{
|
||||
consts::TERM_SIGNALS,
|
||||
flag,
|
||||
iterator::{exfiltrator::WithOrigin, SignalsInfo},
|
||||
low_level,
|
||||
};
|
||||
|
||||
use crate::logging::Logger;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ContainerId {
|
||||
cid_path: PathBuf,
|
||||
requires_sudo: bool,
|
||||
crt: String,
|
||||
}
|
||||
|
||||
impl ContainerId {
|
||||
pub fn new<P, S>(cid_path: P, container_runtime: S, requires_sudo: bool) -> Self
|
||||
where
|
||||
P: Into<PathBuf>,
|
||||
S: Into<String>,
|
||||
{
|
||||
let cid_path = cid_path.into();
|
||||
let crt = container_runtime.into();
|
||||
Self {
|
||||
cid_path,
|
||||
requires_sudo,
|
||||
crt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static PID_LIST: Lazy<Arc<Mutex<Vec<i32>>>> = Lazy::new(|| Arc::new(Mutex::new(vec![])));
|
||||
static CID_LIST: Lazy<Arc<Mutex<Vec<ContainerId>>>> = Lazy::new(|| Arc::new(Mutex::new(vec![])));
|
||||
|
||||
/// Initialize Ctrl-C handler. This should be done at the start
|
||||
/// of a binary.
|
||||
///
|
||||
/// # Panics
|
||||
/// Will panic if initialized more than once.
|
||||
pub fn init<F>(app_exec: F)
|
||||
where
|
||||
F: FnOnce() + Send + 'static,
|
||||
{
|
||||
// Make sure double CTRL+C and similar kills
|
||||
let term_now = Arc::new(AtomicBool::new(false));
|
||||
for sig in TERM_SIGNALS {
|
||||
// When terminated by a second term signal, exit with exit code 1.
|
||||
// This will do nothing the first time (because term_now is false).
|
||||
flag::register_conditional_shutdown(*sig, 1, Arc::clone(&term_now))
|
||||
.expect("Register conditional shutdown");
|
||||
// But this will "arm" the above for the second time, by setting it to true.
|
||||
// The order of registering these is important, if you put this one first, it will
|
||||
// first arm and then terminate ‒ all in the first round.
|
||||
flag::register(*sig, Arc::clone(&term_now)).expect("Register signal");
|
||||
}
|
||||
|
||||
let mut signals = vec![SIGABRT, SIGHUP, SIGTSTP, SIGCONT];
|
||||
signals.extend(TERM_SIGNALS);
|
||||
let mut signals = SignalsInfo::<WithOrigin>::new(signals).expect("Need signal info");
|
||||
|
||||
thread::spawn(app_exec);
|
||||
|
||||
let mut has_terminal = true;
|
||||
for info in &mut signals {
|
||||
match info.signal {
|
||||
termsig if TERM_SIGNALS.contains(&termsig) => {
|
||||
warn!("Received termination signal, cleaning up...");
|
||||
trace!("{info:#?}");
|
||||
|
||||
Logger::multi_progress()
|
||||
.clear()
|
||||
.expect("Should clear multi_progress");
|
||||
|
||||
send_signal_processes(termsig);
|
||||
|
||||
let cid_list = CID_LIST.clone();
|
||||
let cid_list = cid_list.lock().expect("Should lock mutex");
|
||||
cid_list.iter().for_each(|cid| {
|
||||
if let Ok(id) = fs::read_to_string(&cid.cid_path) {
|
||||
let id = id.trim();
|
||||
debug!("Killing container {id}");
|
||||
|
||||
let status = if cid.requires_sudo {
|
||||
Command::new("sudo")
|
||||
.arg(&cid.crt)
|
||||
.arg("stop")
|
||||
.arg(id)
|
||||
.status()
|
||||
} else {
|
||||
Command::new(&cid.crt).arg("stop").arg(id).status()
|
||||
};
|
||||
|
||||
if let Err(e) = status {
|
||||
error!("Failed to kill container {id}: Error {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
drop(cid_list);
|
||||
|
||||
process::exit(1);
|
||||
}
|
||||
SIGTSTP => {
|
||||
if has_terminal {
|
||||
send_signal_processes(SIGTSTP);
|
||||
has_terminal = false;
|
||||
low_level::emulate_default_handler(SIGTSTP).expect("Should stop");
|
||||
}
|
||||
}
|
||||
SIGCONT => {
|
||||
if !has_terminal {
|
||||
send_signal_processes(SIGCONT);
|
||||
has_terminal = true;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
trace!("Received signal {info:#?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_signal_processes(sig: i32) {
|
||||
let pid_list = PID_LIST.clone();
|
||||
let pid_list = pid_list.lock().expect("Should lock mutex");
|
||||
|
||||
pid_list.iter().for_each(|pid| {
|
||||
if let Err(e) = kill(
|
||||
Pid::from_raw(*pid),
|
||||
Signal::try_from(sig).expect("Should be valid signal"),
|
||||
) {
|
||||
error!("Failed to kill process {pid}: Error {e}");
|
||||
} else {
|
||||
trace!("Killed process {pid}");
|
||||
}
|
||||
});
|
||||
drop(pid_list);
|
||||
}
|
||||
|
||||
/// Add a pid to the list to kill when the program
|
||||
/// recieves a kill signal.
|
||||
///
|
||||
/// # Panics
|
||||
/// Will panic if the mutex cannot be locked.
|
||||
pub fn add_pid<T>(pid: T)
|
||||
where
|
||||
T: TryInto<i32>,
|
||||
{
|
||||
if let Ok(pid) = pid.try_into() {
|
||||
let mut pid_list = PID_LIST.lock().expect("Should lock pid_list");
|
||||
|
||||
if !pid_list.contains(&pid) {
|
||||
pid_list.push(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a pid from the list of pids to kill.
|
||||
///
|
||||
/// # Panics
|
||||
/// Will panic if the mutex cannot be locked.
|
||||
pub fn remove_pid<T>(pid: T)
|
||||
where
|
||||
T: TryInto<i32>,
|
||||
{
|
||||
if let Ok(pid) = pid.try_into() {
|
||||
let mut pid_list = PID_LIST.lock().expect("Should lock pid_list");
|
||||
|
||||
if let Some(index) = pid_list.iter().position(|val| *val == pid) {
|
||||
pid_list.swap_remove(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a cid to the list to kill when the program
|
||||
/// recieves a kill signal.
|
||||
///
|
||||
/// # Panics
|
||||
/// Will panic if the mutex cannot be locked.
|
||||
pub fn add_cid(cid: &ContainerId) {
|
||||
let mut cid_list = CID_LIST.lock().expect("Should lock cid_list");
|
||||
|
||||
if !cid_list.contains(cid) {
|
||||
cid_list.push(cid.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a cid from the list of pids to kill.
|
||||
///
|
||||
/// # Panics
|
||||
/// Will panic if the mutex cannot be locked.
|
||||
pub fn remove_cid(cid: &ContainerId) {
|
||||
let mut cid_list = CID_LIST.lock().expect("Should lock cid_list");
|
||||
|
||||
if let Some(index) = cid_list.iter().position(|val| *val == *cid) {
|
||||
cid_list.swap_remove(index);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue