refactor: Move templates to their own crate (#83)
This PR logically separates out parts of the code to their own crates. This will be useful for future Tauri App development.
This commit is contained in:
parent
ce8f889dc2
commit
910e0434b6
34 changed files with 620 additions and 512 deletions
|
|
@ -1,7 +0,0 @@
|
|||
use typed_builder::TypedBuilder;
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder, PartialEq, Eq, Hash)]
|
||||
pub struct AkmodsInfo {
|
||||
pub images: (String, Option<String>),
|
||||
pub stage_name: String,
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@ use log::error;
|
|||
use clap::{command, crate_authors, Parser, Subcommand};
|
||||
use clap_verbosity_flag::{InfoLevel, Verbosity};
|
||||
|
||||
use crate::shadow;
|
||||
|
||||
pub mod bug_report;
|
||||
pub mod build;
|
||||
pub mod completions;
|
||||
|
|
@ -10,7 +12,6 @@ pub mod completions;
|
|||
pub mod init;
|
||||
pub mod local;
|
||||
pub mod template;
|
||||
pub mod utils;
|
||||
|
||||
pub trait BlueBuildCommand {
|
||||
/// Runs the command and returns a result
|
||||
|
|
@ -29,8 +30,6 @@ pub trait BlueBuildCommand {
|
|||
}
|
||||
}
|
||||
|
||||
shadow_rs::shadow!(shadow);
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(
|
||||
name = "BlueBuild",
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
use askama::Template;
|
||||
use blue_build_recipe::Recipe;
|
||||
use blue_build_template::{GithubIssueTemplate, Template};
|
||||
use blue_build_utils::constants::*;
|
||||
use clap::Args;
|
||||
use clap_complete::Shell;
|
||||
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
||||
use log::{debug, error, trace};
|
||||
use requestty::question::{completions, Completions};
|
||||
use std::borrow::Cow;
|
||||
use std::time::Duration;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use super::utils::exec_cmd;
|
||||
use super::BlueBuildCommand;
|
||||
|
||||
use crate::{constants::*, module_recipe::Recipe, shadow};
|
||||
use crate::shadow;
|
||||
|
||||
#[derive(Default, Debug, Clone, TypedBuilder, Args)]
|
||||
pub struct BugReportRecipe {
|
||||
|
|
@ -272,7 +272,7 @@ fn get_shell_version(shell: &str) -> String {
|
|||
error!("Powershell is not supported.");
|
||||
None
|
||||
}
|
||||
_ => exec_cmd(shell, &["--version"], time_limit),
|
||||
_ => blue_build_utils::exec_cmd(shell, &["--version"], time_limit),
|
||||
}
|
||||
.map_or_else(
|
||||
|| UNKNOWN_VERSION.to_string(),
|
||||
|
|
@ -284,52 +284,6 @@ fn get_shell_version(shell: &str) -> String {
|
|||
// Git
|
||||
// ============================================================================= //
|
||||
|
||||
#[derive(Debug, Clone, Template, TypedBuilder)]
|
||||
#[template(path = "github_issue.j2", escape = "md")]
|
||||
struct GithubIssueTemplate<'a> {
|
||||
#[builder(setter(into))]
|
||||
bb_version: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
build_rust_channel: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
build_time: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
git_commit_hash: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
os_name: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
os_version: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
pkg_branch_tag: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
recipe: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
rust_channel: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
rust_version: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
shell_name: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
shell_version: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
terminal_name: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
terminal_version: Cow<'a, str>,
|
||||
}
|
||||
|
||||
fn get_pkg_branch_tag() -> String {
|
||||
format!("{} ({})", shadow::BRANCH, shadow::LAST_TAG)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ use std::{
|
|||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use blue_build_recipe::Recipe;
|
||||
use blue_build_utils::constants::*;
|
||||
use clap::Args;
|
||||
use colorized::{Color, Colors};
|
||||
use log::{debug, info, trace, warn};
|
||||
|
|
@ -35,12 +37,7 @@ use tokio::{
|
|||
sync::oneshot::{self, Sender},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
commands::template::TemplateCommand,
|
||||
constants::{self, *},
|
||||
module_recipe::Recipe,
|
||||
ops,
|
||||
};
|
||||
use crate::commands::template::TemplateCommand;
|
||||
|
||||
use super::BlueBuildCommand;
|
||||
|
||||
|
|
@ -181,19 +178,19 @@ impl BlueBuildCommand for BuildCommand {
|
|||
// -> 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);
|
||||
let container_file_path = Path::new(CONTAINER_FILE);
|
||||
|
||||
if !self.force && container_file_path.exists() {
|
||||
let gitignore = fs::read_to_string(constants::GITIGNORE_PATH)?;
|
||||
let gitignore = fs::read_to_string(GITIGNORE_PATH)?;
|
||||
|
||||
let is_ignored = gitignore
|
||||
.lines()
|
||||
.any(|line: &str| line.contains(constants::CONTAINER_FILE));
|
||||
.any(|line: &str| line.contains(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);
|
||||
let label = format!("LABEL {}", BUILD_ID_LABEL);
|
||||
line.to_string().trim().starts_with(&label)
|
||||
});
|
||||
|
||||
|
|
@ -211,9 +208,9 @@ impl BlueBuildCommand for BuildCommand {
|
|||
|
||||
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),
|
||||
blue_build_utils::append_to_file(
|
||||
GITIGNORE_PATH,
|
||||
&format!("/{}", CONTAINER_FILE),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
|
@ -231,15 +228,15 @@ impl BlueBuildCommand for BuildCommand {
|
|||
.unwrap_or_else(|| PathBuf::from(RECIPE_PATH));
|
||||
|
||||
#[cfg(not(feature = "podman-api"))]
|
||||
if let Err(e1) = ops::check_command_exists("buildah") {
|
||||
ops::check_command_exists("podman").map_err(|e2| {
|
||||
if let Err(e1) = blue_build_utils::check_command_exists("buildah") {
|
||||
blue_build_utils::check_command_exists("podman").map_err(|e2| {
|
||||
anyhow!("Need either 'buildah' or 'podman' commands to proceed: {e1}, {e2}")
|
||||
})?;
|
||||
}
|
||||
|
||||
if self.push {
|
||||
ops::check_command_exists("cosign")?;
|
||||
ops::check_command_exists("skopeo")?;
|
||||
blue_build_utils::check_command_exists("cosign")?;
|
||||
blue_build_utils::check_command_exists("skopeo")?;
|
||||
check_cosign_files()?;
|
||||
}
|
||||
|
||||
|
|
@ -420,8 +417,8 @@ impl BuildCommand {
|
|||
|
||||
info!("Logging into the registry, {registry}");
|
||||
let login_output = match (
|
||||
ops::check_command_exists("buildah"),
|
||||
ops::check_command_exists("podman"),
|
||||
blue_build_utils::check_command_exists("buildah"),
|
||||
blue_build_utils::check_command_exists("podman"),
|
||||
) {
|
||||
(Ok(()), _) => {
|
||||
trace!("buildah login -u {username} -p [MASKED] {registry}");
|
||||
|
|
@ -550,8 +547,8 @@ impl BuildCommand {
|
|||
|
||||
info!("Building image {full_image}");
|
||||
let status = match (
|
||||
ops::check_command_exists("buildah"),
|
||||
ops::check_command_exists("podman"),
|
||||
blue_build_utils::check_command_exists("buildah"),
|
||||
blue_build_utils::check_command_exists("podman"),
|
||||
) {
|
||||
(Ok(()), _) => {
|
||||
trace!("buildah build -t {full_image}");
|
||||
|
|
@ -588,7 +585,7 @@ impl BuildCommand {
|
|||
let retry_count = if retry { self.retry_count } else { 0 };
|
||||
|
||||
// Push images with retries (1s delay between retries)
|
||||
ops::retry(retry_count, 1000, || push_images(tags, image_name))?;
|
||||
blue_build_utils::retry(retry_count, 1000, || push_images(tags, image_name))?;
|
||||
sign_images(image_name, tags.first().map(String::as_str))?;
|
||||
}
|
||||
|
||||
|
|
@ -963,8 +960,8 @@ fn tag_images(tags: &[String], image_name: &str, full_image: &str) -> Result<()>
|
|||
let tag_image = format!("{image_name}:{tag}");
|
||||
|
||||
let status = match (
|
||||
ops::check_command_exists("buildah"),
|
||||
ops::check_command_exists("podman"),
|
||||
blue_build_utils::check_command_exists("buildah"),
|
||||
blue_build_utils::check_command_exists("podman"),
|
||||
) {
|
||||
(Ok(()), _) => {
|
||||
trace!("buildah tag {full_image} {tag_image}");
|
||||
|
|
@ -1001,8 +998,8 @@ fn push_images(tags: &[String], image_name: &str) -> Result<()> {
|
|||
let tag_image = format!("{image_name}:{tag}");
|
||||
|
||||
let status = match (
|
||||
ops::check_command_exists("buildah"),
|
||||
ops::check_command_exists("podman"),
|
||||
blue_build_utils::check_command_exists("buildah"),
|
||||
blue_build_utils::check_command_exists("podman"),
|
||||
) {
|
||||
(Ok(()), _) => {
|
||||
trace!("buildah push {tag_image}");
|
||||
|
|
|
|||
|
|
@ -4,10 +4,9 @@ use std::{
|
|||
};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use blue_build_utils::constants::*;
|
||||
use log::trace;
|
||||
|
||||
use crate::{constants::*, ops};
|
||||
|
||||
#[cfg(feature = "podman-api")]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub enum BuildStrategy {
|
||||
|
|
@ -29,8 +28,8 @@ impl BuildStrategy {
|
|||
PathBuf::from(RUN_PODMAN_SOCK),
|
||||
PathBuf::from(VAR_RUN_PODMAN_PODMAN_SOCK),
|
||||
PathBuf::from(VAR_RUN_PODMAN_SOCK),
|
||||
ops::check_command_exists("buildah"),
|
||||
ops::check_command_exists("podman"),
|
||||
blue_build_utils::check_command_exists("buildah"),
|
||||
blue_build_utils::check_command_exists("podman"),
|
||||
) {
|
||||
(Ok(xdg_runtime), _, _, _, _, _)
|
||||
if Path::new(&format!("{xdg_runtime}/podman/podman.sock")).exists() =>
|
||||
|
|
|
|||
|
|
@ -5,17 +5,14 @@ use std::{
|
|||
};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use blue_build_recipe::Recipe;
|
||||
use blue_build_utils::constants::*;
|
||||
use clap::Args;
|
||||
use log::{debug, info, trace};
|
||||
use typed_builder::TypedBuilder;
|
||||
use users::{Users, UsersCache};
|
||||
|
||||
use crate::{
|
||||
commands::build::BuildCommand,
|
||||
constants::{ARCHIVE_SUFFIX, LOCAL_BUILD},
|
||||
module_recipe::Recipe,
|
||||
ops,
|
||||
};
|
||||
use crate::commands::build::BuildCommand;
|
||||
|
||||
use super::BlueBuildCommand;
|
||||
|
||||
|
|
@ -148,7 +145,7 @@ impl BlueBuildCommand for RebaseCommand {
|
|||
fn check_can_run() -> Result<()> {
|
||||
trace!("check_can_run()");
|
||||
|
||||
ops::check_command_exists("rpm-ostree")?;
|
||||
blue_build_utils::check_command_exists("rpm-ostree")?;
|
||||
|
||||
let cache = UsersCache::new();
|
||||
if cache.get_current_uid() != 0 {
|
||||
|
|
|
|||
|
|
@ -1,57 +1,16 @@
|
|||
use std::{
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
process,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use askama::Template;
|
||||
use blue_build_recipe::Recipe;
|
||||
use blue_build_template::{ContainerFileTemplate, Template};
|
||||
use blue_build_utils::constants::*;
|
||||
use clap::Args;
|
||||
use log::{debug, error, info, trace};
|
||||
use log::{debug, info, trace};
|
||||
use typed_builder::TypedBuilder;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{constants::*, module_recipe::Recipe};
|
||||
|
||||
use super::BlueBuildCommand;
|
||||
|
||||
#[derive(Debug, Clone, Template, TypedBuilder)]
|
||||
#[template(path = "Containerfile.j2", escape = "none")]
|
||||
pub struct ContainerFileTemplate<'a> {
|
||||
recipe: &'a Recipe<'a>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
recipe_path: &'a Path,
|
||||
|
||||
#[builder(setter(into))]
|
||||
build_id: Uuid,
|
||||
|
||||
#[builder(default)]
|
||||
export_script: ExportsTemplate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Template)]
|
||||
#[template(path = "export.sh", escape = "none")]
|
||||
pub struct ExportsTemplate;
|
||||
|
||||
impl ExportsTemplate {
|
||||
fn print_script(&self) -> String {
|
||||
trace!("print_script({self})");
|
||||
|
||||
format!(
|
||||
"\"{}\"",
|
||||
self.render()
|
||||
.unwrap_or_else(|e| {
|
||||
error!("Failed to render export.sh script: {e}");
|
||||
process::exit(1);
|
||||
})
|
||||
.replace('\n', "\\n")
|
||||
.replace('\"', "\\\"")
|
||||
.replace('$', "\\$")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args, TypedBuilder)]
|
||||
pub struct TemplateCommand {
|
||||
/// The recipe file to create a template from
|
||||
|
|
@ -127,48 +86,3 @@ impl TemplateCommand {
|
|||
// ======================================================== //
|
||||
// ========================= Helpers ====================== //
|
||||
// ======================================================== //
|
||||
|
||||
fn has_cosign_file() -> bool {
|
||||
trace!("has_cosign_file()");
|
||||
std::env::current_dir()
|
||||
.map(|p| p.join(COSIGN_PATH).exists())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn print_containerfile(containerfile: &str) -> String {
|
||||
trace!("print_containerfile({containerfile})");
|
||||
debug!("Loading containerfile contents for {containerfile}");
|
||||
|
||||
let path = format!("config/containerfiles/{containerfile}/Containerfile");
|
||||
|
||||
let file = fs::read_to_string(&path).unwrap_or_else(|e| {
|
||||
error!("Failed to read file {path}: {e}");
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
debug!("Containerfile contents {path}:\n{file}");
|
||||
|
||||
file
|
||||
}
|
||||
|
||||
fn get_github_repo_owner() -> Option<String> {
|
||||
Some(env::var(GITHUB_REPOSITORY_OWNER).ok()?.to_lowercase())
|
||||
}
|
||||
|
||||
fn get_gitlab_registry_path() -> Option<String> {
|
||||
Some(
|
||||
format!(
|
||||
"{}/{}/{}",
|
||||
env::var(CI_REGISTRY).ok()?,
|
||||
env::var(CI_PROJECT_NAMESPACE).ok()?,
|
||||
env::var(CI_PROJECT_NAME).ok()?,
|
||||
)
|
||||
.to_lowercase(),
|
||||
)
|
||||
}
|
||||
|
||||
fn modules_exists() -> bool {
|
||||
let mod_path = Path::new("modules");
|
||||
mod_path.exists() && mod_path.is_dir()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,136 +0,0 @@
|
|||
use process_control::{ChildExt, Control};
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::Debug;
|
||||
use std::io::{Error, ErrorKind, Result};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[must_use]
|
||||
pub fn home_dir() -> Option<PathBuf> {
|
||||
directories::BaseDirs::new().map(|base_dirs| base_dirs.home_dir().to_path_buf())
|
||||
}
|
||||
|
||||
// ================================================================================================= //
|
||||
// CommandOutput
|
||||
// ================================================================================================= //
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CommandOutput {
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
}
|
||||
|
||||
/// # Attempt to resolve `binary_name` from and creates a new `Command` pointing at it
|
||||
/// # This allows executing cmd files on Windows and prevents running executable from cwd on Windows
|
||||
/// # This function also initializes std{err,out,in} to protect against processes changing the console mode
|
||||
/// #
|
||||
/// # Errors
|
||||
///
|
||||
pub fn create_command<T: AsRef<OsStr>>(binary_name: T) -> Result<Command> {
|
||||
let binary_name = binary_name.as_ref();
|
||||
log::trace!("Creating Command for binary {:?}", binary_name);
|
||||
|
||||
let full_path = match which::which(binary_name) {
|
||||
Ok(full_path) => {
|
||||
log::trace!("Using {:?} as {:?}", full_path, binary_name);
|
||||
full_path
|
||||
}
|
||||
Err(error) => {
|
||||
log::trace!("Unable to find {:?} in PATH, {:?}", binary_name, error);
|
||||
return Err(Error::new(ErrorKind::NotFound, error));
|
||||
}
|
||||
};
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
let mut cmd = Command::new(full_path);
|
||||
cmd.stderr(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stdin(Stdio::null());
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Execute a command and return the output on stdout and stderr if successful
|
||||
pub fn exec_cmd<T: AsRef<OsStr> + Debug, U: AsRef<OsStr> + Debug>(
|
||||
cmd: T,
|
||||
args: &[U],
|
||||
time_limit: Duration,
|
||||
) -> Option<CommandOutput> {
|
||||
log::trace!("Executing command {:?} with args {:?}", cmd, args);
|
||||
internal_exec_cmd(cmd, args, time_limit)
|
||||
}
|
||||
|
||||
fn internal_exec_cmd<T: AsRef<OsStr> + Debug, U: AsRef<OsStr> + Debug>(
|
||||
cmd: T,
|
||||
args: &[U],
|
||||
time_limit: Duration,
|
||||
) -> Option<CommandOutput> {
|
||||
let mut cmd = create_command(cmd).ok()?;
|
||||
cmd.args(args);
|
||||
exec_timeout(&mut cmd, time_limit)
|
||||
}
|
||||
|
||||
pub fn exec_timeout(cmd: &mut Command, time_limit: Duration) -> Option<CommandOutput> {
|
||||
let start = Instant::now();
|
||||
let process = match cmd.spawn() {
|
||||
Ok(process) => process,
|
||||
Err(error) => {
|
||||
log::trace!("Unable to run {:?}, {:?}", cmd.get_program(), error);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
match process
|
||||
.controlled_with_output()
|
||||
.time_limit(time_limit)
|
||||
.terminate_for_timeout()
|
||||
.wait()
|
||||
{
|
||||
Ok(Some(output)) => {
|
||||
let stdout_string = match String::from_utf8(output.stdout) {
|
||||
Ok(stdout) => stdout,
|
||||
Err(error) => {
|
||||
log::warn!("Unable to decode stdout: {:?}", error);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let stderr_string = match String::from_utf8(output.stderr) {
|
||||
Ok(stderr) => stderr,
|
||||
Err(error) => {
|
||||
log::warn!("Unable to decode stderr: {:?}", error);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
log::trace!(
|
||||
"stdout: {:?}, stderr: {:?}, exit code: \"{:?}\", took {:?}",
|
||||
stdout_string,
|
||||
stderr_string,
|
||||
output.status.code(),
|
||||
start.elapsed()
|
||||
);
|
||||
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(CommandOutput {
|
||||
stdout: stdout_string,
|
||||
stderr: stderr_string,
|
||||
})
|
||||
}
|
||||
Ok(None) => {
|
||||
log::warn!("Executing command {:?} timed out.", cmd.get_program());
|
||||
log::warn!("You can set command_timeout in your config to a higher value to allow longer-running commands to keep executing.");
|
||||
None
|
||||
}
|
||||
Err(error) => {
|
||||
log::trace!(
|
||||
"Executing command {:?} failed by: {:?}",
|
||||
cmd.get_program(),
|
||||
error
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
// 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";
|
||||
pub const VAR_RUN_PODMAN_PODMAN_SOCK: &str = "/var/run/podman/podman.sock";
|
||||
pub const VAR_RUN_PODMAN_SOCK: &str = "/var/run/podman.sock";
|
||||
|
||||
// Labels
|
||||
pub const BUILD_ID_LABEL: &str = "org.blue-build.build-id";
|
||||
|
||||
// Cosign vars
|
||||
pub const COSIGN_PRIVATE_KEY: &str = "COSIGN_PRIVATE_KEY";
|
||||
pub const GITHUB_TOKEN_ISSUER_URL: &str = "https://token.actions.githubusercontent.com";
|
||||
pub const SIGSTORE_ID_TOKEN: &str = "SIGSTORE_ID_TOKEN";
|
||||
|
||||
// GitHub CI vars
|
||||
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_REF_NAME: &str = "GITHUB_REF_NAME";
|
||||
pub const GITHUB_REPOSITORY_OWNER: &str = "GITHUB_REPOSITORY_OWNER";
|
||||
pub const GITHUB_SHA: &str = "GITHUB_SHA";
|
||||
pub const GITHUB_TOKEN: &str = "GH_TOKEN";
|
||||
pub const GITHUB_WORKFLOW_REF: &str = "GITHUB_WORKFLOW_REF";
|
||||
pub const PR_EVENT_NUMBER: &str = "GH_PR_EVENT_NUMBER";
|
||||
|
||||
// GitLab CI vars
|
||||
pub const CI_COMMIT_REF_NAME: &str = "CI_COMMIT_REF_NAME";
|
||||
pub const CI_COMMIT_SHORT_SHA: &str = "CI_COMMIT_SHORT_SHA";
|
||||
pub const CI_DEFAULT_BRANCH: &str = "CI_DEFAULT_BRANCH";
|
||||
pub const CI_MERGE_REQUEST_IID: &str = "CI_MERGE_REQUEST_IID";
|
||||
pub const CI_PIPELINE_SOURCE: &str = "CI_PIPELINE_SOURCE";
|
||||
pub const CI_PROJECT_NAME: &str = "CI_PROJECT_NAME";
|
||||
pub const CI_PROJECT_NAMESPACE: &str = "CI_PROJECT_NAMESPACE";
|
||||
pub const CI_PROJECT_URL: &str = "CI_PROJECT_URL";
|
||||
pub const CI_SERVER_HOST: &str = "CI_SERVER_HOST";
|
||||
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";
|
||||
|
||||
// Terminal vars
|
||||
pub const TERM_PROGRAM: &str = "TERM_PROGRAM";
|
||||
pub const LC_TERMINAL: &str = "LC_TERMINAL";
|
||||
pub const TERM_PROGRAM_VERSION: &str = "TERM_PROGRAM_VERSION";
|
||||
pub const LC_TERMINAL_VERSION: &str = "LC_TERMINAL_VERSION";
|
||||
pub const XDG_RUNTIME_DIR: &str = "XDG_RUNTIME_DIR";
|
||||
|
||||
// Misc
|
||||
pub const UNKNOWN_SHELL: &str = "<unknown shell>";
|
||||
pub const UNKNOWN_VERSION: &str = "<unknown version>";
|
||||
pub const UNKNOWN_TERMINAL: &str = "<unknown terminal>";
|
||||
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?";
|
||||
13
src/lib.rs
13
src/lib.rs
|
|
@ -1,19 +1,6 @@
|
|||
//! The root library for blue-build.
|
||||
#![warn(
|
||||
clippy::correctness,
|
||||
clippy::suspicious,
|
||||
clippy::perf,
|
||||
clippy::style,
|
||||
clippy::nursery
|
||||
)]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![forbid(unsafe_code)]
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
|
||||
shadow_rs::shadow!(shadow);
|
||||
|
||||
pub mod akmods_info;
|
||||
pub mod commands;
|
||||
pub mod constants;
|
||||
pub mod module_recipe;
|
||||
mod ops;
|
||||
|
|
|
|||
|
|
@ -1,397 +0,0 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{HashMap, HashSet},
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
process::{self, Command},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use chrono::Local;
|
||||
use format_serde_error::SerdeError;
|
||||
use indexmap::IndexMap;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use serde_yaml::Value;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use crate::{
|
||||
akmods_info::AkmodsInfo,
|
||||
constants::*,
|
||||
ops::{self, check_command_exists},
|
||||
};
|
||||
|
||||
#[derive(Default, Serialize, Clone, Deserialize, Debug, TypedBuilder)]
|
||||
pub struct Recipe<'a> {
|
||||
#[builder(setter(into))]
|
||||
pub name: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
pub description: Cow<'a, str>,
|
||||
|
||||
#[serde(alias = "base-image")]
|
||||
#[builder(setter(into))]
|
||||
pub base_image: Cow<'a, str>,
|
||||
|
||||
#[serde(alias = "image-version")]
|
||||
#[builder(setter(into))]
|
||||
pub image_version: Cow<'a, str>,
|
||||
|
||||
#[serde(alias = "blue-build-tag")]
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub blue_build_tag: Option<Cow<'a, str>>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub modules_ext: ModuleExt<'a>,
|
||||
|
||||
#[serde(flatten)]
|
||||
#[builder(setter(into))]
|
||||
pub extra: IndexMap<String, Value>,
|
||||
}
|
||||
|
||||
impl<'a> Recipe<'a> {
|
||||
#[must_use]
|
||||
pub fn generate_tags(&self) -> Vec<String> {
|
||||
trace!("Recipe::generate_tags()");
|
||||
trace!("Generating image tags for {}", &self.name);
|
||||
|
||||
let mut tags: Vec<String> = Vec::new();
|
||||
let image_version = self.get_os_version();
|
||||
let timestamp = Local::now().format("%Y%m%d").to_string();
|
||||
|
||||
if let (Ok(commit_branch), Ok(default_branch), Ok(commit_sha), Ok(pipeline_source)) = (
|
||||
env::var(CI_COMMIT_REF_NAME),
|
||||
env::var(CI_DEFAULT_BRANCH),
|
||||
env::var(CI_COMMIT_SHORT_SHA),
|
||||
env::var(CI_PIPELINE_SOURCE),
|
||||
) {
|
||||
trace!("CI_COMMIT_REF_NAME={commit_branch}, CI_DEFAULT_BRANCH={default_branch},CI_COMMIT_SHORT_SHA={commit_sha}, CI_PIPELINE_SOURCE={pipeline_source}");
|
||||
warn!("Detected running in Gitlab, pulling information from CI variables");
|
||||
|
||||
if let Ok(mr_iid) = env::var(CI_MERGE_REQUEST_IID) {
|
||||
trace!("CI_MERGE_REQUEST_IID={mr_iid}");
|
||||
if pipeline_source == "merge_request_event" {
|
||||
debug!("Running in a MR");
|
||||
tags.push(format!("mr-{mr_iid}-{image_version}"));
|
||||
}
|
||||
}
|
||||
|
||||
if default_branch == commit_branch {
|
||||
debug!("Running on the default branch");
|
||||
tags.push(image_version.to_string());
|
||||
tags.push(format!("{timestamp}-{image_version}"));
|
||||
tags.push("latest".into());
|
||||
tags.push(timestamp);
|
||||
} else {
|
||||
debug!("Running on branch {commit_branch}");
|
||||
tags.push(format!("br-{commit_branch}-{image_version}"));
|
||||
}
|
||||
|
||||
tags.push(format!("{commit_sha}-{image_version}"));
|
||||
} else if let (
|
||||
Ok(github_event_name),
|
||||
Ok(github_event_number),
|
||||
Ok(github_sha),
|
||||
Ok(github_ref_name),
|
||||
) = (
|
||||
env::var(GITHUB_EVENT_NAME),
|
||||
env::var(PR_EVENT_NUMBER),
|
||||
env::var(GITHUB_SHA),
|
||||
env::var(GITHUB_REF_NAME),
|
||||
) {
|
||||
trace!("GITHUB_EVENT_NAME={github_event_name},PR_EVENT_NUMBER={github_event_number},GITHUB_SHA={github_sha},GITHUB_REF_NAME={github_ref_name}");
|
||||
warn!("Detected running in Github, pulling information from GITHUB variables");
|
||||
|
||||
let mut short_sha = github_sha;
|
||||
short_sha.truncate(7);
|
||||
|
||||
if github_event_name == "pull_request" {
|
||||
debug!("Running in a PR");
|
||||
tags.push(format!("pr-{github_event_number}-{image_version}"));
|
||||
} else if github_ref_name == "live" || github_ref_name == "main" {
|
||||
tags.push(image_version.to_string());
|
||||
tags.push(format!("{timestamp}-{image_version}"));
|
||||
tags.push("latest".into());
|
||||
tags.push(timestamp);
|
||||
} else {
|
||||
tags.push(format!("br-{github_ref_name}-{image_version}"));
|
||||
}
|
||||
tags.push(format!("{short_sha}-{image_version}"));
|
||||
} else {
|
||||
warn!("Running locally");
|
||||
tags.push(format!("local-{image_version}"));
|
||||
}
|
||||
debug!("Finished generating tags!");
|
||||
debug!("Tags: {tags:#?}");
|
||||
|
||||
tags
|
||||
}
|
||||
|
||||
/// # Parse a recipe file
|
||||
/// #
|
||||
/// # Errors
|
||||
pub fn parse<P: AsRef<Path>>(path: &P) -> Result<Self> {
|
||||
let file_path = if Path::new(path.as_ref()).is_absolute() {
|
||||
path.as_ref().to_path_buf()
|
||||
} else {
|
||||
std::env::current_dir()?.join(path.as_ref())
|
||||
};
|
||||
|
||||
let recipe_path = fs::canonicalize(file_path)?;
|
||||
let recipe_path_string = recipe_path.display().to_string();
|
||||
debug!("Recipe::parse_recipe({recipe_path_string})");
|
||||
|
||||
let file = fs::read_to_string(recipe_path)?;
|
||||
|
||||
debug!("Recipe contents: {file}");
|
||||
|
||||
let mut recipe =
|
||||
serde_yaml::from_str::<Recipe>(&file).map_err(ops::serde_yaml_err(&file))?;
|
||||
|
||||
recipe.modules_ext.modules = Module::get_modules(&recipe.modules_ext.modules).into();
|
||||
|
||||
Ok(recipe)
|
||||
}
|
||||
|
||||
pub fn get_os_version(&self) -> String {
|
||||
trace!("Recipe::get_os_version()");
|
||||
|
||||
if check_command_exists("skopeo").is_err() {
|
||||
warn!("The 'skopeo' command doesn't exist, falling back to version defined in recipe");
|
||||
return self.image_version.to_string();
|
||||
}
|
||||
|
||||
let base_image = self.base_image.as_ref();
|
||||
let image_version = self.image_version.as_ref();
|
||||
|
||||
info!("Retrieving information from {base_image}:{image_version}, this will take a bit");
|
||||
|
||||
let output = match Command::new("skopeo")
|
||||
.arg("inspect")
|
||||
.arg(format!("docker://{base_image}:{image_version}"))
|
||||
.output()
|
||||
{
|
||||
Err(_) => {
|
||||
warn!(
|
||||
"Issue running the 'skopeo' command, falling back to version defined in recipe"
|
||||
);
|
||||
return self.image_version.to_string();
|
||||
}
|
||||
Ok(output) => output,
|
||||
};
|
||||
|
||||
if !output.status.success() {
|
||||
warn!("Failed to get image information for {base_image}:{image_version}, falling back to version defined in recipe");
|
||||
return self.image_version.to_string();
|
||||
}
|
||||
|
||||
let inspection: ImageInspection = match serde_json::from_str(
|
||||
String::from_utf8_lossy(&output.stdout).as_ref(),
|
||||
) {
|
||||
Err(err) => {
|
||||
let err_msg =
|
||||
SerdeError::new(String::from_utf8_lossy(&output.stdout).to_string(), err)
|
||||
.to_string();
|
||||
warn!("Issue deserializing 'skopeo' output, falling back to version defined in recipe. {err_msg}",);
|
||||
return self.image_version.to_string();
|
||||
}
|
||||
Ok(inspection) => inspection,
|
||||
};
|
||||
|
||||
inspection.get_version().unwrap_or_else(|| {
|
||||
warn!("Version label does not exist on image, using version in recipe");
|
||||
image_version.to_string()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Clone, Deserialize, Debug, TypedBuilder)]
|
||||
pub struct ModuleExt<'a> {
|
||||
#[builder(default, setter(into))]
|
||||
pub modules: Cow<'a, [Module<'a>]>,
|
||||
}
|
||||
|
||||
impl ModuleExt<'_> {
|
||||
/// # Parse a module file returning a [`ModuleExt`]
|
||||
///
|
||||
/// # Errors
|
||||
/// Can return an `anyhow` Error if the file cannot be read or deserialized
|
||||
/// into a [`ModuleExt`]
|
||||
pub fn parse_module_from_file(file_name: &str) -> Result<Self> {
|
||||
let file_path = PathBuf::from("config").join(file_name);
|
||||
let file_path = if file_path.is_absolute() {
|
||||
file_path
|
||||
} else {
|
||||
std::env::current_dir()?.join(file_path)
|
||||
};
|
||||
|
||||
let file = fs::read_to_string(file_path)?;
|
||||
|
||||
serde_yaml::from_str::<Self>(&file).map_or_else(
|
||||
|_| -> Result<Self> {
|
||||
let module =
|
||||
serde_yaml::from_str::<Module>(&file).map_err(ops::serde_yaml_err(&file))?;
|
||||
Ok(Self::builder().modules(vec![module]).build())
|
||||
},
|
||||
Ok,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_akmods_info_list(&self, os_version: &str) -> Vec<AkmodsInfo> {
|
||||
trace!("get_akmods_image_list({self:#?}, {os_version})");
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
self.modules
|
||||
.iter()
|
||||
.filter(|module| module.module_type.as_ref().is_some_and(|t| t == "akmods"))
|
||||
.map(|module| module.generate_akmods_info(os_version))
|
||||
.filter(|image| seen.insert(image.clone()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, TypedBuilder)]
|
||||
pub struct Module<'a> {
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
|
||||
pub module_type: Option<Cow<'a, str>>,
|
||||
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
#[serde(rename = "from-file", skip_serializing_if = "Option::is_none")]
|
||||
pub from_file: Option<Cow<'a, str>>,
|
||||
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub source: Option<Cow<'a, str>>,
|
||||
|
||||
#[serde(flatten)]
|
||||
#[builder(default, setter(into))]
|
||||
pub config: IndexMap<String, Value>,
|
||||
}
|
||||
|
||||
impl<'a> Module<'a> {
|
||||
#[must_use]
|
||||
pub fn get_modules(modules: &[Self]) -> Vec<Self> {
|
||||
modules
|
||||
.iter()
|
||||
.flat_map(|module| {
|
||||
module.from_file.as_ref().map_or_else(
|
||||
|| vec![module.clone()],
|
||||
|file_name| match ModuleExt::parse_module_from_file(file_name) {
|
||||
Err(e) => {
|
||||
error!("Failed to get module from {file_name}: {e}");
|
||||
vec![]
|
||||
}
|
||||
Ok(module_ext) => Self::get_modules(&module_ext.modules),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_module_type_list(&'a self, typ: &str, list_key: &str) -> Option<Vec<String>> {
|
||||
if self.module_type.as_ref()? == typ {
|
||||
Some(
|
||||
self.config
|
||||
.get(list_key)?
|
||||
.as_sequence()?
|
||||
.iter()
|
||||
.filter_map(|t| Some(t.as_str()?.to_owned()))
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_containerfile_list(&'a self) -> Option<Vec<String>> {
|
||||
self.get_module_type_list("containerfile", "containerfiles")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_containerfile_snippets(&'a self) -> Option<Vec<String>> {
|
||||
self.get_module_type_list("containerfile", "snippets")
|
||||
}
|
||||
|
||||
pub fn print_module_context(&'a self) -> String {
|
||||
serde_json::to_string(self).unwrap_or_else(|e| {
|
||||
error!("Failed to parse module!!!!!: {e}");
|
||||
process::exit(1);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_files_list(&'a self) -> Option<Vec<(String, String)>> {
|
||||
Some(
|
||||
self.config
|
||||
.get("files")?
|
||||
.as_sequence()?
|
||||
.iter()
|
||||
.filter_map(|entry| entry.as_mapping())
|
||||
.flatten()
|
||||
.filter_map(|(src, dest)| {
|
||||
Some((
|
||||
format!("./config/files/{}", src.as_str()?),
|
||||
dest.as_str()?.to_string(),
|
||||
))
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn generate_akmods_info(&'a self, os_version: &str) -> AkmodsInfo {
|
||||
trace!("generate_akmods_base({self:#?}, {os_version})");
|
||||
|
||||
let base = self
|
||||
.config
|
||||
.get("base")
|
||||
.map(|b| b.as_str().unwrap_or_default());
|
||||
let nvidia_version = self
|
||||
.config
|
||||
.get("nvidia-version")
|
||||
.map(|v| v.as_u64().unwrap_or_default());
|
||||
|
||||
AkmodsInfo::builder()
|
||||
.images(match (base, nvidia_version) {
|
||||
(Some(b), Some(nv)) if !b.is_empty() && nv > 0 => (
|
||||
format!("akmods:{b}-{os_version}"),
|
||||
Some(format!("akmods-nvidia:{b}-{os_version}-{nv}")),
|
||||
),
|
||||
(Some(b), _) if !b.is_empty() => (format!("akmods:{b}-{os_version}"), None),
|
||||
(_, Some(nv)) if nv > 0 => (
|
||||
format!("akmods:main-{os_version}"),
|
||||
Some(format!("akmods-nvidia:main-{os_version}")),
|
||||
),
|
||||
_ => (format!("akmods:main-{os_version}"), None),
|
||||
})
|
||||
.stage_name(format!(
|
||||
"{}{}",
|
||||
base.unwrap_or("main"),
|
||||
nvidia_version.map_or_else(String::default, |nv| format!("-{nv}"))
|
||||
))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
struct ImageInspection {
|
||||
#[serde(alias = "Labels")]
|
||||
labels: HashMap<String, JsonValue>,
|
||||
}
|
||||
|
||||
impl ImageInspection {
|
||||
pub fn get_version(&self) -> Option<String> {
|
||||
Some(
|
||||
self.labels
|
||||
.get("org.opencontainers.image.version")?
|
||||
.as_str()
|
||||
.map(std::string::ToString::to_string)?
|
||||
.split('.')
|
||||
.take(1)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
70
src/ops.rs
70
src/ops.rs
|
|
@ -1,70 +0,0 @@
|
|||
use std::{io::Write, process::Command};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use format_serde_error::SerdeError;
|
||||
use log::{debug, trace};
|
||||
use std::{thread, time::Duration};
|
||||
|
||||
pub fn check_command_exists(command: &str) -> Result<()> {
|
||||
trace!("check_command_exists({command})");
|
||||
debug!("Checking if {command} exists");
|
||||
|
||||
trace!("which {command}");
|
||||
if Command::new("which")
|
||||
.arg(command)
|
||||
.output()?
|
||||
.status
|
||||
.success()
|
||||
{
|
||||
debug!("Command {command} does exist");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Command {command} doesn't exist and is required to build the image"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
let location = location.as_ref();
|
||||
SerdeError::new(
|
||||
contents.to_string(),
|
||||
(
|
||||
err.into(),
|
||||
location.map_or(0, serde_yaml::Location::line).into(),
|
||||
location.map_or(0, serde_yaml::Location::column).into(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn retry<V, F>(mut attempts: u8, delay: u64, f: F) -> anyhow::Result<V>
|
||||
where
|
||||
F: Fn() -> anyhow::Result<V>,
|
||||
{
|
||||
loop {
|
||||
match f() {
|
||||
Ok(v) => return Ok(v),
|
||||
Err(e) if attempts == 1 => return Err(e),
|
||||
_ => {
|
||||
attempts -= 1;
|
||||
thread::sleep(Duration::from_secs(delay));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue