Start work on build command

This commit is contained in:
Gerald Pinder 2023-12-17 15:34:32 -05:00
parent bcd7e710a2
commit 71d93977b9
7 changed files with 226 additions and 18 deletions

View file

@ -1,2 +1,2 @@
[language-server.rust-analyzer.config]
cargo.features = ["init"]
cargo.features = ["build"]

3
Cargo.lock generated
View file

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

View file

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

View file

@ -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<PathBuf>,
#[arg(short, long, default_value = "Containerfile")]
output: PathBuf,
#[arg(short, long)]
push: bool,
#[arg(long)]
registry: Option<String>,
#[arg(long)]
registry_path: Option<String>,
#[arg(long)]
username: Option<String>,
#[arg(long)]
password: Option<String>,
},
}
@ -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!();
}
}

152
src/build.rs Normal file
View file

@ -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<String> {
let mut tags: Vec<String> = 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(&registry)
.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(&registry)
.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<String> {
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(())
}

View file

@ -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<PathBuf>) -> Result<(Tera, Context)> {
let recipe_de =
serde_yaml::from_str::<Recipe>(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::<Recipe>(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<PathBuf>) -> 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(())
}

View file

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