From 71d93977b91cca8ad14bc1bc312988b042926333 Mon Sep 17 00:00:00 2001 From: Gerald Pinder Date: Sun, 17 Dec 2023 15:34:32 -0500 Subject: [PATCH] Start work on build command --- .helix/languages.toml | 2 +- Cargo.lock | 3 + Cargo.toml | 1 + src/bin/ublue.rs | 58 ++++++++++++---- src/build.rs | 152 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 26 ++++++-- src/module_recipe.rs | 2 + 7 files changed, 226 insertions(+), 18 deletions(-) create mode 100644 src/build.rs diff --git a/.helix/languages.toml b/.helix/languages.toml index d3b5352..ff86c4d 100644 --- a/.helix/languages.toml +++ b/.helix/languages.toml @@ -1,2 +1,2 @@ [language-server.rust-analyzer.config] -cargo.features = ["init"] +cargo.features = ["build"] diff --git a/Cargo.lock b/Cargo.lock index d8990ee..fdfda47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,7 +140,9 @@ checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", "windows-targets", ] @@ -788,6 +790,7 @@ version = "0.2.2" dependencies = [ "anyhow", "cfg-if", + "chrono", "clap", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index de17701..7e5e984 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ exclude = [".gitlab-ci.yml", ".helix/", ".git/", ".gitignore"] [dependencies] anyhow = "1.0.75" cfg-if = "1.0.0" +chrono = "0.4.31" clap = { version = "4.4.4", features = ["derive"] } serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.107" diff --git a/src/bin/ublue.rs b/src/bin/ublue.rs index c2f1e5b..c44b3fc 100644 --- a/src/bin/ublue.rs +++ b/src/bin/ublue.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use anyhow::Result; use clap::{Parser, Subcommand}; @@ -17,7 +17,7 @@ enum CommandArgs { Template { /// The recipe file to create a template from #[arg()] - recipe: String, + recipe: PathBuf, /// Optional Containerfile to use as a template #[arg(short, long)] @@ -39,8 +39,30 @@ enum CommandArgs { /// Build an image from a Containerfile #[cfg(feature = "build")] Build { + /// The recipe file to create a template from #[arg()] - containerfile: String, + recipe: PathBuf, + + #[arg(short, long)] + containerfile: Option, + + #[arg(short, long, default_value = "Containerfile")] + output: PathBuf, + + #[arg(short, long)] + push: bool, + + #[arg(long)] + registry: Option, + + #[arg(long)] + registry_path: Option, + + #[arg(long)] + username: Option, + + #[arg(long)] + password: Option, }, } @@ -53,14 +75,7 @@ fn main() -> Result<()> { containerfile, output, } => { - let (tera, context) = ublue_rs::setup_tera(recipe, containerfile)?; - let output_str = tera.render("Containerfile", &context)?; - - if let Some(output) = output { - std::fs::write(output, output_str)?; - } else { - println!("{output_str}"); - } + ublue_rs::template_file(&recipe, containerfile.as_ref(), output.as_ref())?; } #[cfg(feature = "init")] CommandArgs::Init { dir } => { @@ -72,8 +87,25 @@ fn main() -> Result<()> { ublue_rs::init::initialize_directory(base_dir); } #[cfg(feature = "build")] - CommandArgs::Build { containerfile: _ } => { - println!("Not yet implemented!"); + CommandArgs::Build { + recipe, + containerfile, + output, + push, + registry, + registry_path, + username, + password, + } => { + ublue_rs::template_file(&recipe, containerfile.as_ref(), Some(&output))?; + ublue_rs::build::build_image( + &recipe, + registry.as_ref(), + registry_path.as_ref(), + username.as_ref(), + password.as_ref(), + push, + )?; todo!(); } } diff --git a/src/build.rs b/src/build.rs new file mode 100644 index 0000000..40a1508 --- /dev/null +++ b/src/build.rs @@ -0,0 +1,152 @@ +use std::{env, fs, path::Path, process::Command}; + +use anyhow::{anyhow, Result}; +use chrono::{Datelike, Local}; + +use crate::module_recipe::Recipe; + +fn check_command_exists(command: &str) -> Result<()> { + match Command::new("command") + .arg("-v") + .arg(command) + .status()? + .success() + { + true => Ok(()), + false => Err(anyhow!( + "Command {command} doesn't exist and is required to build the image" + )), + } +} + +fn generate_tags(recipe: &Recipe) -> Vec { + let mut tags: Vec = Vec::new(); + let image_version = recipe.image_version; + let timestamp = Local::now().format("%Y%m%d").to_string(); + + if let Ok(_) = env::var("CI") { + if let (Ok(mr_iid), Ok(pipeline_source)) = ( + env::var("CI_MERGE_REQUEST_IID"), + env::var("CI_PIPELINE_SOURCE"), + ) { + if pipeline_source == "merge_request_event" { + tags.push(format!("{mr_iid}-{image_version}")); + } + } + + if let Ok(commit_sha) = env::var("CI_COMMIT_SHORT_SHA") { + tags.push(format!("{commit_sha}-{image_version}")); + } + + if let (Ok(commit_branch), Ok(default_branch)) = + (env::var("CI_COMMIT_BRANCH"), env::var("CI_DEFAULT_BRANCH")) + { + if default_branch != commit_branch { + tags.push(format!("br-{commit_branch}-{image_version}")); + } else { + tags.push(format!("{image_version}")); + tags.push(format!("{image_version}-{timestamp}")); + tags.push(format!("{timestamp}")); + } + } + } else { + tags.push(format!("{image_version}-local")); + } + tags +} + +fn login( + registry: Option<&String>, + username: Option<&String>, + password: Option<&String>, +) -> Result<()> { + let registry = match registry { + Some(registry) => registry.to_owned(), + None => env::var("CI_REGISTRY")?, + }; + + let username = match username { + Some(username) => username.to_owned(), + None => env::var("CI_REGISTRY_USER")?, + }; + + let password = match password { + Some(password) => password.to_owned(), + None => env::var("CI_REGISTRY_PASSWORD")?, + }; + + match Command::new("buildah") + .arg("login") + .arg("-u") + .arg(&username) + .arg("-p") + .arg(&password) + .arg(®istry) + .status()? + .success() + { + true => eprintln!("Buildah login success!"), + false => return Err(anyhow!("Failed to login for buildah!")), + } + + match Command::new("cosign") + .arg("login") + .arg("-u") + .arg(&username) + .arg("-p") + .arg(&password) + .arg(®istry) + .status()? + .success() + { + true => eprintln!("Cosign login success!"), + false => return Err(anyhow!("Failed to login for cosign!")), + } + + Ok(()) +} + +fn generate_full_image_name( + recipe: &Recipe, + registry: Option<&String>, + registry_path: Option<&String>, +) -> Result { + let image_name = recipe.name.as_str(); + + if let Ok(_) = env::var("CI") { + // if let (Ok()) + todo!() + } else { + Ok(image_name.to_string()) + } +} + +fn build(recipe: &Recipe, image_name: &str, tags: &[String]) -> Result<()> { + todo!() +} + +pub fn build_image( + recipe: &Path, + registry: Option<&String>, + registry_path: Option<&String>, + username: Option<&String>, + password: Option<&String>, + push: bool, +) -> Result<()> { + check_command_exists("buildah")?; + if push { + check_command_exists("cosign")?; + check_command_exists("skopeo")?; + login(registry.clone(), username.clone(), password.clone())?; + } + + let recipe: Recipe = serde_yaml::from_str(fs::read_to_string(recipe)?.as_str())?; + + let tags = generate_tags(&recipe); + + let image_name = generate_full_image_name(&recipe, registry.clone(), registry_path.clone())?; + + build(&recipe, &image_name, &tags)?; + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 28ab635..54dd07b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,12 +11,15 @@ #[cfg(feature = "init")] pub mod init; +#[cfg(feature = "build")] +pub mod build; + pub mod module_recipe; use std::{ collections::HashMap, fs::{self, read_to_string}, - path::PathBuf, + path::{Path, PathBuf}, }; use anyhow::Result; @@ -25,9 +28,8 @@ use tera::{Context, Tera}; pub const DEFAULT_CONTAINERFILE: &str = include_str!("../templates/Containerfile.tera"); -pub fn setup_tera(recipe: String, containerfile: Option) -> Result<(Tera, Context)> { - let recipe_de = - serde_yaml::from_str::(fs::read_to_string(PathBuf::from(&recipe))?.as_str())?; +fn setup_tera(recipe: &Path, containerfile: Option<&PathBuf>) -> Result<(Tera, Context)> { + let recipe_de = serde_yaml::from_str::(fs::read_to_string(&recipe)?.as_str())?; let mut context = Context::from_serialize(recipe_de)?; context.insert("recipe", &recipe); @@ -94,3 +96,19 @@ pub fn setup_tera(recipe: String, containerfile: Option) -> Result<(Ter Ok((tera, context)) } + +pub fn template_file( + recipe: &Path, + containerfile: Option<&PathBuf>, + output: Option<&PathBuf>, +) -> Result<()> { + let (tera, context) = setup_tera(recipe, containerfile)?; + let output_str = tera.render("Containerfile", &context)?; + + if let Some(output) = output { + std::fs::write(output, output_str)?; + } else { + println!("{output_str}"); + } + Ok(()) +} diff --git a/src/module_recipe.rs b/src/module_recipe.rs index 0031ef0..5e7e28c 100644 --- a/src/module_recipe.rs +++ b/src/module_recipe.rs @@ -7,6 +7,8 @@ use serde_yaml::Value; pub struct Recipe { pub name: String, + pub description: String, + #[serde(alias = "base-image")] pub base_image: String,