From 85aadf73e520b5f0c14c2e98093745c45a52b0c1 Mon Sep 17 00:00:00 2001 From: Gerald Pinder <717217-gmpinder@users.noreply.gitlab.com> Date: Fri, 27 Oct 2023 20:26:14 +0000 Subject: [PATCH] feat(modules)!: Support new modules based starting point template --- .helix/languages.toml | 3 + Cargo.lock | 1 + Cargo.toml | 5 +- README.md | 12 +- src/bin/ublue.rs | 5 +- src/lib.rs | 106 +++++++++++++++--- src/module_recipe.rs | 40 +++++++ ...ng_point.template => Containerfile.legacy} | 0 templates/Containerfile.modules | 59 ++++++++++ 9 files changed, 208 insertions(+), 23 deletions(-) create mode 100644 .helix/languages.toml create mode 100644 src/module_recipe.rs rename templates/{starting_point.template => Containerfile.legacy} (100%) create mode 100644 templates/Containerfile.modules diff --git a/.helix/languages.toml b/.helix/languages.toml new file mode 100644 index 0000000..d876fee --- /dev/null +++ b/.helix/languages.toml @@ -0,0 +1,3 @@ +[[language]] +name = "rust" +config = { cargo = { features = [ "modules" ], noDefaultFeatures = false } } diff --git a/Cargo.lock b/Cargo.lock index 2ae88c9..710edc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -787,6 +787,7 @@ name = "ublue-rs" version = "0.1.1" dependencies = [ "anyhow", + "cfg-if", "clap", "serde", "serde_yaml", diff --git a/Cargo.toml b/Cargo.toml index 7d69a44..8ad1e1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,13 +12,16 @@ exclude = [".gitlab-ci.yml"] [dependencies] anyhow = "1.0.75" +cfg-if = "1.0.0" clap = { version = "4.4.4", features = ["derive"] } serde = { version = "1.0.188", features = ["derive"] } serde_yaml = "0.9.25" tera = "1.19.1" [features] -default = [] +default = ["modules"] nightly = ["init", "build"] init = [] build = [] +modules = [] +legacy = [] diff --git a/README.md b/README.md index 1451e95..6fbcac1 100644 --- a/README.md +++ b/README.md @@ -10,19 +10,27 @@ Right now the only way to install this tool is to use `cargo`. cargo install --locked ublue-rs ``` +### Legacy Starting Point + +If you want to install the tool for use with the legacy setup of the starting point template, you can install it with: + +```bash +cargo install --locked --features legacy --no-default-features ublue-rs +``` + ## How to use Once you have the CLI tool installed, you can run the following to pull in your recipe file to generate a `Containerfile`. ```bash -ublue template recipe.yml -o Containerfile +ublue template -o ``` You can then use this with `podman` to build and publish your image. Further options can be viewed by running `ublue --help` ## Future Features -- [ ] Update to the most recent stable style of the [starting point](https://github.com/ublue-os/startingpoint/tree/template) template +- [x] Update to the most recent stable style of the [starting point](https://github.com/ublue-os/startingpoint/tree/template) template - [ ] Create an init command to create a repo for you to start out - [ ] Setup the project to allow installing with `binstall` - [ ] Create an install script for easy install for users without `cargo` diff --git a/src/bin/ublue.rs b/src/bin/ublue.rs index 4395c81..42af249 100644 --- a/src/bin/ublue.rs +++ b/src/bin/ublue.rs @@ -13,13 +13,14 @@ fn main() -> Result<()> { } => { 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}"); } } - #[cfg(init)] + #[cfg(feature = "init")] CommandArgs::Init { dir } => { let base_dir = match dir { Some(dir) => dir, @@ -28,7 +29,7 @@ fn main() -> Result<()> { ublue_rs::init::initialize_directory(base_dir); } - #[cfg(build)] + #[cfg(feature = "build")] CommandArgs::Build { containerfile: _ } => { println!("Not yet implemented!"); todo!(); diff --git a/src/lib.rs b/src/lib.rs index 0364aba..bfca20f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,20 +1,46 @@ +//! The root library for ublue-rs. +//! +//! This module consists of the args for the cli as well as the +//! initial entrypoint for setting up tera to properly template +//! the Containerfile. There is support for legacy starting point +//! recipes using the feature flag 'legacy' and support for the newest +//! starting point setup using the 'modules' feature flag. You will not want +//! to use both features at the same time. For now the 'legacy' feature +//! is the default feature until modules works 1-1 with ublue starting point. + +#[cfg(all(feature = "legacy", feature = "modules"))] +compile_error!("Both 'legacy' and 'modules' features cannot be used at the same time."); + +#[cfg(feature = "init")] +pub mod init; + +#[cfg(feature = "legacy")] +pub mod recipe; + +#[cfg(feature = "modules")] +pub mod module_recipe; + use std::{ collections::HashMap, - fs::{self, read_dir, read_to_string}, + fs::{self, read_to_string}, path::PathBuf, }; use anyhow::Result; +use cfg_if; use clap::{Parser, Subcommand}; -use recipe::Recipe; -use tera::{from_value, Context, Tera}; +use tera::{Context, Tera}; -pub const DEFAULT_CONTAINERFILE: &'static str = - include_str!("../templates/starting_point.template"); - -#[cfg(init)] -pub mod init; -pub mod recipe; +cfg_if::cfg_if! { + if #[cfg(feature = "legacy")] { + use recipe::Recipe; + use std::fs::read_dir; + pub const DEFAULT_CONTAINERFILE: &str = include_str!("../templates/Containerfile.legacy"); + } else if #[cfg(feature = "modules")] { + use module_recipe::Recipe; + pub const DEFAULT_CONTAINERFILE: &str = include_str!("../templates/Containerfile.modules"); + } +} #[derive(Parser, Debug)] #[command(name = "Ublue Builder", author, version, about, long_about = None)] @@ -41,7 +67,7 @@ pub enum CommandArgs { }, /// Initialize a new Ublue Starting Point repo - #[cfg(init)] + #[cfg(feature = "init")] Init { /// The directory to extract the files into. Defaults to the current directory #[arg()] @@ -49,7 +75,7 @@ pub enum CommandArgs { }, /// Build an image from a Containerfile - #[cfg(build)] + #[cfg(feature = "build")] Build { #[arg()] containerfile: String, @@ -58,33 +84,77 @@ pub enum CommandArgs { 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())? - .process_repos(); + serde_yaml::from_str::(fs::read_to_string(PathBuf::from(&recipe))?.as_str())?; + + #[cfg(feature = "legacy")] + let recipe_de = recipe_de.process_repos(); let mut context = Context::from_serialize(recipe_de)?; context.insert("recipe", &recipe); let mut tera = Tera::default(); + match containerfile { Some(containerfile) => { tera.add_raw_template("Containerfile", &read_to_string(containerfile)?)? } None => tera.add_raw_template("Containerfile", DEFAULT_CONTAINERFILE)?, } + tera.register_function( "print_containerfile", |args: &HashMap| -> tera::Result { match args.get("containerfile") { - Some(v) => match from_value::(v.clone()) { - Ok(containerfile) => { - Ok(read_to_string(format!("containerfiles/{containerfile}"))?.into()) - } - Err(_) => Err("Arg containerfile wasn't a string".into()), + Some(v) => match v.as_str() { + Some(containerfile) => Ok(read_to_string(format!( + "containerfiles/{containerfile}/Containerfile" + ))? + .into()), + None => Err("Arg containerfile wasn't a string".into()), }, None => Err("Needs the argument 'containerfile'".into()), } }, ); + + #[cfg(feature = "modules")] + tera.register_function( + "print_module_context", + |args: &HashMap| -> tera::Result { + match args.get("module") { + Some(v) => Ok(match serde_yaml::to_string(v) { + Ok(s) => s.escape_default().collect::(), + Err(_) => "Unable to serialize".into(), + } + .into()), + None => Err("Needs the argument 'module'".into()), + } + }, + ); + + #[cfg(feature = "modules")] + tera.register_function( + "get_module_from_file", + |args: &HashMap| -> tera::Result { + match args.get("file") { + Some(v) => { + let file = match v.as_str() { + Some(s) => s, + None => return Err("Property 'from-file' must be a string".into()), + }; + match serde_yaml::from_str::( + read_to_string(format!("config/{file}"))?.as_str(), + ) { + Ok(context) => Ok(context), + Err(_) => Err(format!("Unable to deserialize file {file}").into()), + } + } + None => Err("Needs the argument 'file'".into()), + } + }, + ); + + #[cfg(feature = "legacy")] tera.register_function( "print_autorun_scripts", |args: &HashMap| -> tera::Result { diff --git a/src/module_recipe.rs b/src/module_recipe.rs new file mode 100644 index 0000000..0031ef0 --- /dev/null +++ b/src/module_recipe.rs @@ -0,0 +1,40 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use serde_yaml::Value; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Recipe { + pub name: String, + + #[serde(alias = "base-image")] + pub base_image: String, + + #[serde(alias = "image-version")] + pub image_version: u16, + + pub modules: Vec, + + pub containerfiles: Option, + + #[serde(flatten)] + pub extra: HashMap, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Module { + #[serde(rename = "type")] + pub module_type: Option, + + #[serde(rename = "from-file")] + pub from_file: Option, + + #[serde(flatten)] + pub config: HashMap, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Containerfiles { + pub pre: Option>, + pub post: Option>, +} diff --git a/templates/starting_point.template b/templates/Containerfile.legacy similarity index 100% rename from templates/starting_point.template rename to templates/Containerfile.legacy diff --git a/templates/Containerfile.modules b/templates/Containerfile.modules new file mode 100644 index 0000000..aaea00b --- /dev/null +++ b/templates/Containerfile.modules @@ -0,0 +1,59 @@ +FROM {{ base_image }}:{{ image_version }} + +ARG RECIPE={{ recipe }} + +# Copy the bling from ublue-os/bling into tmp, to be installed later by the bling module +# Feel free to remove these lines if you want to speed up image builds and don't want any bling +COPY --from=ghcr.io/ublue-os/bling:latest /rpms /tmp/bling/rpms +COPY --from=ghcr.io/ublue-os/bling:latest /files /tmp/bling/files + +COPY --from=docker.io/mikefarah/yq /usr/bin/yq /usr/bin/yq + +COPY --from=gcr.io/projectsigstore/cosign /ko-app/cosign /usr/bin/cosign + +COPY config /tmp/config/ + +# Copy modules +# The default modules are inside ublue-os/bling +COPY --from=ghcr.io/ublue-os/bling:latest /modules /tmp/modules/ +# Custom modules overwrite defaults +COPY modules /tmp/modules/ + +RUN echo "#!/usr/bin/env bash" >> /tmp/exports.sh +RUN echo 'get_yaml_array() { readarray "$1" < <(echo "$3" | yq -I=0 "$2"); }; export -f get_yaml_array' >> /tmp/exports.sh +RUN chmod +x /tmp/exports.sh + +ARG CONFIG_DIRECTORY="/tmp/config" +ARG IMAGE_NAME="{{ name }}" +ARG BASE_IMAGE="{{ base_image }}" + +{% if containerfiles and containerfiles.pre %} +# Pre: Containerfiles + {% for containerfile in containerfiles.pre %} +{{ print_containerfile(containerfile = containerfile) }} + {% endfor %} +{% endif %} + +{% macro run_modules(module) %} +{% if module.type %} +RUN chmod +x /tmp/modules/{{ module.type }}/{{ module.type }}.sh +RUN source /tmp/exports.sh && OS_VERSION="$(grep -Po '(?<=VERSION_ID=)\d+' /usr/lib/os-release)" /tmp/modules/{{ module.type }}/{{ module.type }}.sh $(echo -e "{{ print_module_context(module = module) }}") +{% elif module["from-file"] %} +{% set extra_module = get_module_from_file(file = module["from-file"]) %} +{% for m in extra_module.modules %} +{{ self::run_modules(module = m) }} +{% endfor %} +{% endif %} +{% endmacro run_modules %} + +{% for module in modules %} +{{ self::run_modules(module = module) }} +{% endfor %} + +{% if containerfiles and containerfiles.post %} + {% for containerfile in containerfiles.post %} +{{ print_containerfile(containerfile = containerfile) }} + {% endfor %} +{% endif %} + +RUN rm -rf /tmp/* /var/* && ostree container commit