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:
Gerald Pinder 2024-02-25 15:45:33 -05:00 committed by GitHub
parent ce8f889dc2
commit 910e0434b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 620 additions and 512 deletions

View file

@ -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,
}

View file

@ -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",

View file

@ -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)
}

View file

@ -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}");

View file

@ -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() =>

View file

@ -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 {

View file

@ -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()
}

View file

@ -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
}
}
}

View file

@ -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?";

View file

@ -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;

View file

@ -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(),
)
}
}

View file

@ -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));
}
};
}
}