feat(iso): Create generate-iso command (#192)
## Tasks - [x] Add ctrl-c handler to kill spawned children - [x] add more args to support all variables - [x] Add integration test
This commit is contained in:
parent
4634f40840
commit
e6cce3d542
25 changed files with 737 additions and 201 deletions
|
|
@ -39,6 +39,9 @@ fn main() {
|
|||
#[cfg(feature = "login")]
|
||||
CommandArgs::Login(mut command) => command.run(),
|
||||
|
||||
#[cfg(feature = "iso")]
|
||||
CommandArgs::GenerateIso(mut command) => command.run(),
|
||||
|
||||
CommandArgs::BugReport(mut command) => command.run(),
|
||||
|
||||
CommandArgs::Completions(mut command) => command.run(),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ pub mod bug_report;
|
|||
pub mod build;
|
||||
pub mod completions;
|
||||
pub mod generate;
|
||||
#[cfg(feature = "iso")]
|
||||
pub mod generate_iso;
|
||||
#[cfg(feature = "login")]
|
||||
pub mod login;
|
||||
// #[cfg(feature = "init")]
|
||||
|
|
@ -68,6 +70,10 @@ pub enum CommandArgs {
|
|||
#[clap(visible_alias = "template")]
|
||||
Generate(generate::GenerateCommand),
|
||||
|
||||
/// Generate an ISO for an image or recipe.
|
||||
#[cfg(feature = "iso")]
|
||||
GenerateIso(generate_iso::GenerateIsoCommand),
|
||||
|
||||
/// Upgrade your current OS with the
|
||||
/// local image saved at `/etc/bluebuild/`.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use std::{
|
|||
};
|
||||
|
||||
use blue_build_process_management::drivers::{
|
||||
opts::{BuildTagPushOpts, CheckKeyPairOpts, CompressionType},
|
||||
opts::{BuildTagPushOpts, CheckKeyPairOpts, CompressionType, GenerateTagsOpts, SignVerifyOpts},
|
||||
BuildDriver, CiDriver, Driver, DriverArgs, SigningDriver,
|
||||
};
|
||||
use blue_build_recipe::Recipe;
|
||||
|
|
@ -14,11 +14,13 @@ use blue_build_utils::{
|
|||
GITIGNORE_PATH, LABELED_ERROR_MESSAGE, NO_LABEL_ERROR_MESSAGE, RECIPE_FILE, RECIPE_PATH,
|
||||
},
|
||||
credentials::{Credentials, CredentialsArgs},
|
||||
string,
|
||||
};
|
||||
use clap::Args;
|
||||
use colored::Colorize;
|
||||
use log::{debug, info, trace, warn};
|
||||
use miette::{bail, Context, IntoDiagnostic, Result};
|
||||
use oci_distribution::Reference;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use crate::commands::generate::GenerateCommand;
|
||||
|
|
@ -197,7 +199,6 @@ impl BlueBuildCommand for BuildCommand {
|
|||
impl BuildCommand {
|
||||
#[cfg(feature = "multi-recipe")]
|
||||
fn start(&self, recipe_paths: &[PathBuf]) -> Result<()> {
|
||||
use blue_build_process_management::drivers::opts::SignVerifyOpts;
|
||||
use rayon::prelude::*;
|
||||
|
||||
trace!("BuildCommand::build_image()");
|
||||
|
|
@ -205,54 +206,12 @@ impl BuildCommand {
|
|||
recipe_paths
|
||||
.par_iter()
|
||||
.try_for_each(|recipe_path| -> Result<()> {
|
||||
let recipe = Recipe::parse(recipe_path)?;
|
||||
let containerfile = if recipe_paths.len() > 1 {
|
||||
blue_build_utils::generate_containerfile_path(recipe_path)?
|
||||
} else {
|
||||
PathBuf::from(CONTAINER_FILE)
|
||||
};
|
||||
let tags = Driver::generate_tags(&recipe)?;
|
||||
let image_name = self.generate_full_image_name(&recipe)?;
|
||||
|
||||
let opts = if let Some(archive_dir) = self.archive.as_ref() {
|
||||
BuildTagPushOpts::builder()
|
||||
.containerfile(&containerfile)
|
||||
.archive_path(format!(
|
||||
"{}/{}.{ARCHIVE_SUFFIX}",
|
||||
archive_dir.to_string_lossy().trim_end_matches('/'),
|
||||
recipe.name.to_lowercase().replace('/', "_"),
|
||||
))
|
||||
.squash(self.squash)
|
||||
.build()
|
||||
} else {
|
||||
BuildTagPushOpts::builder()
|
||||
.image(&image_name)
|
||||
.containerfile(&containerfile)
|
||||
.tags(&tags)
|
||||
.push(self.push)
|
||||
.retry_push(self.retry_push)
|
||||
.retry_count(self.retry_count)
|
||||
.compression(self.compression_format)
|
||||
.squash(self.squash)
|
||||
.build()
|
||||
};
|
||||
|
||||
Driver::build_tag_push(&opts)?;
|
||||
|
||||
if self.push && !self.no_sign {
|
||||
let opts = SignVerifyOpts::builder()
|
||||
.image(&image_name)
|
||||
.retry_push(self.retry_push)
|
||||
.retry_count(self.retry_count);
|
||||
let opts = if let Some(tag) = tags.first() {
|
||||
opts.tag(tag).build()
|
||||
} else {
|
||||
opts.build()
|
||||
};
|
||||
Driver::sign_and_verify(&opts)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
self.build(recipe_path, &containerfile)
|
||||
})?;
|
||||
|
||||
info!("Build complete!");
|
||||
|
|
@ -261,18 +220,27 @@ impl BuildCommand {
|
|||
|
||||
#[cfg(not(feature = "multi-recipe"))]
|
||||
fn start(&self, recipe_path: &Path) -> Result<()> {
|
||||
use blue_build_process_management::drivers::opts::SignVerifyOpts;
|
||||
|
||||
trace!("BuildCommand::start()");
|
||||
|
||||
self.build(recipe_path, Path::new(CONTAINER_FILE))?;
|
||||
|
||||
info!("Build complete!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build(&self, recipe_path: &Path, containerfile: &Path) -> Result<()> {
|
||||
let recipe = Recipe::parse(recipe_path)?;
|
||||
let containerfile = PathBuf::from(CONTAINER_FILE);
|
||||
let tags = Driver::generate_tags(&recipe)?;
|
||||
let image_name = self.generate_full_image_name(&recipe)?;
|
||||
let tags = Driver::generate_tags(
|
||||
&GenerateTagsOpts::builder()
|
||||
.oci_ref(&recipe.base_image_ref()?)
|
||||
.alt_tags(recipe.alt_tags())
|
||||
.build(),
|
||||
)?;
|
||||
let image_name = self.image_name(&recipe)?;
|
||||
|
||||
let opts = if let Some(archive_dir) = self.archive.as_ref() {
|
||||
BuildTagPushOpts::builder()
|
||||
.containerfile(&containerfile)
|
||||
.containerfile(containerfile)
|
||||
.archive_path(format!(
|
||||
"{}/{}.{ARCHIVE_SUFFIX}",
|
||||
archive_dir.to_string_lossy().trim_end_matches('/'),
|
||||
|
|
@ -283,7 +251,7 @@ impl BuildCommand {
|
|||
} else {
|
||||
BuildTagPushOpts::builder()
|
||||
.image(&image_name)
|
||||
.containerfile(&containerfile)
|
||||
.containerfile(containerfile)
|
||||
.tags(&tags)
|
||||
.push(self.push)
|
||||
.retry_push(self.retry_push)
|
||||
|
|
@ -308,14 +276,29 @@ impl BuildCommand {
|
|||
Driver::sign_and_verify(&opts)?;
|
||||
}
|
||||
|
||||
info!("Build complete!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn image_name(&self, recipe: &Recipe) -> Result<String> {
|
||||
let image_name = self.generate_full_image_name(recipe)?;
|
||||
|
||||
let image_name = if image_name.registry().is_empty() {
|
||||
string!(image_name.repository())
|
||||
} else {
|
||||
format!(
|
||||
"{}/{}",
|
||||
image_name.resolve_registry(),
|
||||
image_name.repository()
|
||||
)
|
||||
};
|
||||
|
||||
Ok(image_name)
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return `Err` if the image name cannot be generated.
|
||||
fn generate_full_image_name(&self, recipe: &Recipe) -> Result<String> {
|
||||
fn generate_full_image_name(&self, recipe: &Recipe) -> Result<Reference> {
|
||||
trace!("BuildCommand::generate_full_image_name({recipe:#?})");
|
||||
info!("Generating full image name");
|
||||
|
||||
|
|
@ -324,14 +307,18 @@ impl BuildCommand {
|
|||
self.registry_namespace.as_ref().map(|s| s.to_lowercase()),
|
||||
) {
|
||||
trace!("registry={registry}, registry_path={registry_path}");
|
||||
format!(
|
||||
let image = format!(
|
||||
"{}/{}/{}",
|
||||
registry.trim().trim_matches('/'),
|
||||
registry_path.trim().trim_matches('/'),
|
||||
recipe.name.trim(),
|
||||
)
|
||||
);
|
||||
image
|
||||
.parse()
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Unable to parse {image}"))?
|
||||
} else {
|
||||
Driver::generate_image_name(recipe)?
|
||||
Driver::generate_image_name(&recipe.name)?
|
||||
};
|
||||
|
||||
debug!("Using image name '{image_name}'");
|
||||
|
|
|
|||
|
|
@ -93,15 +93,15 @@ impl GenerateCommand {
|
|||
});
|
||||
|
||||
debug!("Deserializing recipe");
|
||||
let recipe_de = Recipe::parse(&recipe_path)?;
|
||||
trace!("recipe_de: {recipe_de:#?}");
|
||||
let recipe = Recipe::parse(&recipe_path)?;
|
||||
trace!("recipe_de: {recipe:#?}");
|
||||
|
||||
if self.display_full_recipe {
|
||||
if let Some(output) = self.output.as_ref() {
|
||||
std::fs::write(output, serde_yaml::to_string(&recipe_de).into_diagnostic()?)
|
||||
std::fs::write(output, serde_yaml::to_string(&recipe).into_diagnostic()?)
|
||||
.into_diagnostic()?;
|
||||
} else {
|
||||
syntax_highlighting::print_ser(&recipe_de, "yml", self.syntax_theme)?;
|
||||
syntax_highlighting::print_ser(&recipe, "yml", self.syntax_theme)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -109,9 +109,9 @@ impl GenerateCommand {
|
|||
info!("Templating for recipe at {}", recipe_path.display());
|
||||
|
||||
let template = ContainerFileTemplate::builder()
|
||||
.os_version(Driver::get_os_version(&recipe_de)?)
|
||||
.os_version(Driver::get_os_version(&recipe.base_image_ref()?)?)
|
||||
.build_id(Driver::get_build_id())
|
||||
.recipe(&recipe_de)
|
||||
.recipe(&recipe)
|
||||
.recipe_path(recipe_path.as_path())
|
||||
.registry(Driver::get_registry()?)
|
||||
.repo(Driver::get_repo_url()?)
|
||||
|
|
@ -142,7 +142,3 @@ impl GenerateCommand {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================================== //
|
||||
// ========================= Helpers ====================== //
|
||||
// ======================================================== //
|
||||
|
|
|
|||
240
src/commands/generate_iso.rs
Normal file
240
src/commands/generate_iso.rs
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
use std::{
|
||||
env, fs,
|
||||
path::{self, Path, PathBuf},
|
||||
};
|
||||
|
||||
use blue_build_recipe::Recipe;
|
||||
use blue_build_utils::{constants::ARCHIVE_SUFFIX, string_vec};
|
||||
use clap::{Args, Subcommand, ValueEnum};
|
||||
use miette::{bail, Context, IntoDiagnostic, Result};
|
||||
use oci_distribution::Reference;
|
||||
use tempdir::TempDir;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use blue_build_process_management::{
|
||||
drivers::{opts::RunOpts, Driver, DriverArgs, RunDriver},
|
||||
run_volumes,
|
||||
};
|
||||
|
||||
use super::{build::BuildCommand, BlueBuildCommand};
|
||||
|
||||
#[derive(Clone, Debug, TypedBuilder, Args)]
|
||||
pub struct GenerateIsoCommand {
|
||||
#[command(subcommand)]
|
||||
command: GenIsoSubcommand,
|
||||
|
||||
/// The directory to save the resulting ISO file.
|
||||
#[arg(short, long)]
|
||||
output_dir: Option<PathBuf>,
|
||||
|
||||
/// The variant of the installer to use.
|
||||
///
|
||||
/// The Kinoite variant will ask for a user
|
||||
/// and password before installing the OS.
|
||||
/// This version is the most stable and is
|
||||
/// recommended.
|
||||
///
|
||||
/// The Silverblue variant will ask for a user
|
||||
/// and password on first boot after the OS
|
||||
/// is installed.
|
||||
///
|
||||
/// The Server variant is the basic installer
|
||||
/// and will ask to setup a user at install time.
|
||||
#[arg(short = 'V', long, default_value = "server")]
|
||||
variant: GenIsoVariant,
|
||||
|
||||
/// The url to the secure boot public key.
|
||||
///
|
||||
/// Defaults to one of UBlue's public key.
|
||||
/// It's recommended to change this if your base
|
||||
/// image is not from UBlue.
|
||||
#[arg(
|
||||
long,
|
||||
default_value = "https://github.com/ublue-os/bazzite/raw/main/secure_boot.der"
|
||||
)]
|
||||
secure_boot_url: String,
|
||||
|
||||
/// The enrollment password for the secure boot
|
||||
/// key.
|
||||
///
|
||||
/// Default's to UBlue's enrollment password.
|
||||
/// It's recommended to change this if your base
|
||||
/// image is not from UBlue.
|
||||
#[arg(long, default_value = "universalblue")]
|
||||
enrollment_password: String,
|
||||
|
||||
/// The name of your ISO image file.
|
||||
#[arg(long)]
|
||||
iso_name: Option<String>,
|
||||
|
||||
#[clap(flatten)]
|
||||
#[builder(default)]
|
||||
drivers: DriverArgs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
pub enum GenIsoSubcommand {
|
||||
/// Build an ISO from a remote image.
|
||||
Image {
|
||||
/// The image ref to create the iso from.
|
||||
#[arg()]
|
||||
image: String,
|
||||
},
|
||||
/// Build an ISO from a recipe.
|
||||
///
|
||||
/// This will build the image locally first
|
||||
/// before creating the ISO. This is a long
|
||||
/// process.
|
||||
Recipe {
|
||||
/// The path to the recipe file for your image.
|
||||
#[arg()]
|
||||
recipe: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, ValueEnum)]
|
||||
pub enum GenIsoVariant {
|
||||
#[default]
|
||||
Kinoite,
|
||||
Silverblue,
|
||||
Server,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for GenIsoVariant {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match *self {
|
||||
Self::Server => "Server",
|
||||
Self::Silverblue => "Silverblue",
|
||||
Self::Kinoite => "Kinoite",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl BlueBuildCommand for GenerateIsoCommand {
|
||||
fn try_run(&mut self) -> Result<()> {
|
||||
Driver::init(self.drivers);
|
||||
|
||||
let image_out_dir = TempDir::new("build_image").into_diagnostic()?;
|
||||
|
||||
let output_dir = if let Some(output_dir) = self.output_dir.clone() {
|
||||
if output_dir.exists() && !output_dir.is_dir() {
|
||||
bail!("The '--output-dir' arg must be a directory");
|
||||
}
|
||||
|
||||
if !output_dir.exists() {
|
||||
fs::create_dir(&output_dir).into_diagnostic()?;
|
||||
}
|
||||
|
||||
path::absolute(output_dir).into_diagnostic()?
|
||||
} else {
|
||||
env::current_dir().into_diagnostic()?
|
||||
};
|
||||
|
||||
if let GenIsoSubcommand::Recipe { recipe } = &self.command {
|
||||
#[cfg(feature = "multi-recipe")]
|
||||
let mut build_command = {
|
||||
BuildCommand::builder()
|
||||
.recipe(vec![recipe.clone()])
|
||||
.archive(image_out_dir.path())
|
||||
.build()
|
||||
};
|
||||
#[cfg(not(feature = "multi-recipe"))]
|
||||
let mut build_command = {
|
||||
BuildCommand::builder()
|
||||
.recipe(recipe.to_path_buf())
|
||||
.archive(image_out_dir.path())
|
||||
.build()
|
||||
};
|
||||
|
||||
build_command.try_run()?;
|
||||
}
|
||||
|
||||
let iso_name = self.iso_name.as_ref().map_or("deploy.iso", String::as_str);
|
||||
let iso_path = output_dir.join(iso_name);
|
||||
|
||||
if iso_path.exists() {
|
||||
fs::remove_file(iso_path).into_diagnostic()?;
|
||||
}
|
||||
|
||||
self.build_iso(iso_name, &output_dir, image_out_dir.path())
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateIsoCommand {
|
||||
fn build_iso(&self, iso_name: &str, output_dir: &Path, image_out_dir: &Path) -> Result<()> {
|
||||
let mut args = string_vec![
|
||||
format!("VARIANT={}", self.variant),
|
||||
format!("ISO_NAME=build/{iso_name}"),
|
||||
"DNF_CACHE=/cache/dnf",
|
||||
format!("SECURE_BOOT_KEY_URL={}", self.secure_boot_url),
|
||||
format!("ENROLLMENT_PASSWORD={}", self.enrollment_password),
|
||||
];
|
||||
let mut vols = run_volumes![
|
||||
output_dir.display().to_string() => "/build-container-installer/build",
|
||||
"dnf-cache" => "/cache/dnf/",
|
||||
];
|
||||
|
||||
match &self.command {
|
||||
GenIsoSubcommand::Image { image } => {
|
||||
let image: Reference = image
|
||||
.parse()
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Unable to parse image reference {image}"))?;
|
||||
let (image_repo, image_name) = {
|
||||
let registry = image.resolve_registry();
|
||||
let repo = image.repository();
|
||||
let image = format!("{registry}/{repo}");
|
||||
|
||||
let mut image_parts = image.split('/').collect::<Vec<_>>();
|
||||
let image_name = image_parts.pop().unwrap(); // Should be at least 2 elements
|
||||
let image_repo = image_parts.join("/");
|
||||
(image_repo, image_name.to_string())
|
||||
};
|
||||
|
||||
args.extend([
|
||||
format!("IMAGE_NAME={image_name}",),
|
||||
format!("IMAGE_REPO={image_repo}"),
|
||||
format!("IMAGE_TAG={}", image.tag().unwrap_or("latest")),
|
||||
format!("VERSION={}", Driver::get_os_version(&image)?),
|
||||
]);
|
||||
}
|
||||
GenIsoSubcommand::Recipe { recipe } => {
|
||||
let recipe = Recipe::parse(recipe)?;
|
||||
|
||||
args.extend([
|
||||
format!(
|
||||
"IMAGE_SRC=oci-archive:/img_src/{}.{ARCHIVE_SUFFIX}",
|
||||
recipe.name.replace('/', "_"),
|
||||
),
|
||||
format!(
|
||||
"VERSION={}",
|
||||
Driver::get_os_version(&recipe.base_image_ref()?)?,
|
||||
),
|
||||
]);
|
||||
vols.extend(run_volumes![
|
||||
image_out_dir.display().to_string() => "/img_src/",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Currently testing local tarball builds
|
||||
let opts = RunOpts::builder()
|
||||
.image("ghcr.io/jasonn3/build-container-installer")
|
||||
.privileged(true)
|
||||
.remove(true)
|
||||
.args(&args)
|
||||
.volumes(vols)
|
||||
.build();
|
||||
|
||||
let status = Driver::run(&opts).into_diagnostic()?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("Failed to create ISO");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue