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

25
template/Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
[package]
name = "blue-build-template"
version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
askama = { version = "0.12", features = ["serde-json", "serde-yaml"] }
blue-build-recipe = { path = "../recipe" }
blue-build-utils = { path = "../utils" }
log.workspace = true
serde.workspace = true
serde_yaml.workspace = true
serde_json.workspace = true
typed-builder.workspace = true
uuid.workspace = true
[lints]
workspace = true

137
template/src/lib.rs Normal file
View file

@ -0,0 +1,137 @@
use std::{borrow::Cow, env, fs, path::Path, process};
use blue_build_recipe::Recipe;
use blue_build_utils::constants::*;
use log::{debug, error, trace};
use typed_builder::TypedBuilder;
use uuid::Uuid;
pub use askama::Template;
#[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, Template, TypedBuilder)]
#[template(path = "github_issue.j2", escape = "md")]
pub 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 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

@ -0,0 +1,59 @@
{%- let os_version = recipe.get_os_version() %}
# This stage is responsible for holding onto
# your config without copying it directly into
# the final image
FROM scratch as stage-config
COPY ./config /config
# Copy modules
# The default modules are inside blue-build/modules
# Custom modules overwrite defaults
FROM scratch as stage-modules
COPY --from=ghcr.io/blue-build/modules:latest /modules /modules
{%- if self::modules_exists() %}
COPY ./modules /modules
{%- endif %}
{%- include "modules/akmods/akmods.j2" %}
# This stage is responsible for holding onto
# exports like the exports.sh
FROM docker.io/alpine as stage-exports
RUN printf {{ export_script.print_script() }} >> /exports.sh && chmod +x /exports.sh
FROM {{ recipe.base_image }}:{{ recipe.image_version }}
LABEL {{ blue_build_utils::constants::BUILD_ID_LABEL }}="{{ build_id }}"
LABEL org.opencontainers.image.title="{{ recipe.name }}"
LABEL org.opencontainers.image.description="{{ recipe.description }}"
LABEL io.artifacthub.package.readme-url=https://raw.githubusercontent.com/blue-build/cli/main/README.md
ARG RECIPE={{ recipe_path.display() }}
{%- if let Some(repo_owner) = self::get_github_repo_owner() %}
ARG IMAGE_REGISTRY=ghcr.io/{{ repo_owner }}
{%- else if let Some(registry) = self::get_gitlab_registry_path() %}
ARG IMAGE_REGISTRY={{ registry }}
{%- else %}
ARG IMAGE_REGISTRY=localhost
{%- endif %}
{%- if self::has_cosign_file() %}
COPY cosign.pub /usr/share/ublue-os/cosign.pub
{%- endif %}
ARG CONFIG_DIRECTORY="/tmp/config"
ARG IMAGE_NAME="{{ recipe.name }}"
ARG BASE_IMAGE="{{ recipe.base_image }}"
{%- include "modules/modules.j2" %}
COPY --from=gcr.io/projectsigstore/cosign /ko-app/cosign /usr/bin/cosign
COPY --from=ghcr.io/blue-build/cli:
{%- if let Some(tag) = recipe.blue_build_tag -%}
{{ tag }}
{%- else -%}
latest-installer
{%- endif %} /out/bluebuild /usr/bin/bluebuild
RUN ostree container commit

View file

@ -0,0 +1,8 @@
#!/usr/bin/env bash
get_yaml_array() {
readarray -t "$1" < <(echo "$3" | yq -I=0 "$2")
}
export -f get_yaml_array
export OS_VERSION=$(grep -Po '(?<=VERSION_ID=)\d+' /usr/lib/os-release)

View file

@ -0,0 +1,35 @@
#### Current Behavior
<!-- A clear and concise description of the behavior. -->
#### Expected Behavior
<!-- A clear and concise description of what you expected to happen. -->
#### Additional context/Screenshots
<!-- Add any other context about the problem here. If applicable, add screenshots to help explain. -->
#### Possible Solution
<!--- Only if you have suggestions on a fix for the bug -->
#### Environment
- Blue Build Version: {{ bb_version }}
- Operating system: {{ os_name }} {{ os_version }}
- Branch/Tag: {{ pkg_branch_tag }}
- Git Commit Hash: {{ git_commit_hash }}
#### Shell
- Name: {{ shell_name }}
- Version: {{ shell_version }}
- Terminal emulator: {{ terminal_name }} {{ terminal_version }}
#### Rust
- Rust Version: {{ rust_version }}
- Rust channel: {{ rust_channel }} {{ build_rust_channel }}
- Build Time: {{ build_time }}
{%- if !recipe.is_empty() %}
#### Recipe:
```yml
{{ recipe }}
```
{%- endif %}

View file

@ -0,0 +1,7 @@
{%- for info in recipe.modules_ext.get_akmods_info_list(os_version) %}
FROM scratch as stage-akmods-{{ info.stage_name }}
COPY --from=ghcr.io/ublue-os/{{ info.images.0 }} /rpms /rpms
{%- if let Some(nv_image) = info.images.1 %}
COPY --from=ghcr.io/ublue-os/{{ nv_image }} /rpms /rpms
{%- endif %}
{%- endfor %}

View file

@ -0,0 +1,76 @@
# `containerfile`
:::caution
Only compiler-based builds can use this module as it is built-in to the BlueBuild CLI tool.
:::
The `containerfile` module is a tool for adding custom [`Containerfile`](https://github.com/containers/common/blob/main/docs/Containerfile.5.md) instructions for custom image builds. This is useful when you wish to use some feature directly available in a `Containerfile`, but not in a bash module, such as copying from other OCI images with `COPY --from`.
Since standard compiler-based BlueBuild image builds generate a `Containerfile` from your recipe, there is no need to manage it yourself. However, we know that we also have technical users that would like to have the ability to customize their `Containerfile`. This is where the `containerfile` module comes into play.
## Usage
### `snippets:`
The `snippets` property is the easiest to use when you just need to insert a few custom lines to the `Containerfile`. Each entry under the `snippets` property will be directly inserted into your final `Containerfile` for your build.
```yaml
modules:
- type: containerfile
snippets:
- COPY --from=docker.io/mikefarah/yq /usr/bin/yq /usr/bin/yq
```
This makes it really easy to copy a file or program from another image.
:::note
**NOTE:** Each entry of a snippet will be its own layer in the final `Containerfile`.
:::
### `containerfiles:`
The `containerfiles` property allows you to tell the compiler which directory contains a `Containerfile` in `./config/containerfiles/`.
Below is an example of how a `containerfile` module would be used with the `containerfiles` property:
```yaml
modules:
- type: containerfile
containerfiles:
- example
- subroutine
```
In the example above, the compiler would look for these files:
- `./config/containerfiles/example/Containerfile`
- `./config/containerfiles/subroutine/Containerfile`
You could then store files related to say the `subroutine` `Containerfile` in `./config/containerfiles/subroutine/` to keep it organized and portable for other recipes to use.
:::note
**NOTE:** The instructions you add in your `Containerfile`'s each become a layer unlike other modules which are typically run as a single `RUN` command, thus creating only one layer.
:::
### Order of operations
The order of operations is important in a `Containerfile`. There's a very simple set of rules for the order in this module:
- For each defined `containerfile` module:
- First all `containerfiles:` are added to the main `Containerfile` in the order they are defined
- Then all `snippets` are added to the main `Containerfile` in the order they are defined
If you wanted to have some `snippets` run before any `containerfiles` have, you will want to put them in their own module definition before the entry for `containerfiles`. For example:
```yaml
modules:
- type: containerfile
snippets:
- COPY --from=docker.io/mikefarah/yq /usr/bin/yq /usr/bin/yq
- type: containerfile
containerfiles:
- example
- subroutine
```
In the example above, the `COPY` from the `snippets` will always come before the `containerfiles` "example" and "subroutine".

View file

@ -0,0 +1,10 @@
{%- if let Some(containerfiles) = module.get_containerfile_list() %}
{%- for c in containerfiles %}
{{ self::print_containerfile(c) }}
{%- endfor %}
{%- endif %}
{%- if let Some(snippets) = module.get_containerfile_snippets() %}
{%- for s in snippets %}
{{ s }}
{%- endfor %}
{%- endif %}

View file

@ -0,0 +1,10 @@
name: containerfile
shortdesc: The containerfile module enables the addition of custom Containerfile instructions into the build process.
readme: https://raw.githubusercontent.com/blue-build/cli/main/templates/modules/containerfile/README.md
example: |
type: containerfile
snippets:
- COPY ./config/example-dir/example-file.txt /usr/etc/example/
containerfiles:
- example
- subroutine

View file

@ -0,0 +1,15 @@
# `files`
The `files` module simplifies the process of copying files to the image during the build time. These files are sourced from the `config/files` directory, which is located at `/tmp/config/files` inside the image.
:::note
If you want to place any files in `/etc/`, you should place them in `/usr/etc/` instead, which will be used to generate `/etc/` on a booted system. That is the proper directory for "system" configuration templates on atomic Fedora distros, whereas `/etc/` is meant for manual overrides and editing by the machine's admin AFTER installation! See issue https://github.com/blue-build/legacy-template/issues/28.
:::
## Implementation differences between the legacy template and compiler-based builds
When using a compiler-based build (which is the recommended option for all users, so if you don't know what you're using you're probably using that), each instruction under `files:` creates its on layer in the final image using the `Containerfile` `COPY`-command. This module is entirely part of the recipe compiler.
When using a legacy template, all modules are combined into one layer in the final image. With a repo based on the legacy template, the bash version is used.
The API for both of these options remains exactly the same.

View file

@ -0,0 +1,5 @@
{%- if let Some(files) = module.get_files_list() %}
{%- for (src, dest) in files %}
COPY {{ src }} {{ dest }}
{%- endfor %}
{%- endif %}

View file

@ -0,0 +1,9 @@
name: files
shortdesc: The files module simplifies the process of copying files to the image during build time.
readme: https://raw.githubusercontent.com/blue-build/cli/main/templates/modules/files/README.md
example: |
type: files
files:
- usr: /usr
# usr: file/folder inside config/files/ to copy (config/files/usr/ in the repository)
# /usr: destination on the final system

View file

@ -0,0 +1,29 @@
{%- for module in recipe.modules_ext.modules %}
{%- if let Some(type) = module.module_type %}
{%- if type == "containerfile" %}
{%- include "modules/containerfile/containerfile.j2" %}
{%- else if type == "files" %}
{%- include "modules/files/files.j2" %}
{%- else %}
RUN \
--mount=type=tmpfs,target=/tmp \
--mount=type=tmpfs,target=/var \
--mount=type=bind,from=docker.io/mikefarah/yq,src=/usr/bin/yq,dst=/usr/bin/yq \
--mount=type=bind,from=stage-config,src=/config,dst=/tmp/config,rw \
{%- if let Some(source) = module.source %}
--mount=type=bind,from={{ source }},src=/modules,dst=/tmp/modules,rw \
{%- else %}
--mount=type=bind,from=stage-modules,src=/modules,dst=/tmp/modules,rw \
{%- endif %}
{%- if type == "akmods" %}
--mount=type=bind,from=stage-akmods-{{ module.generate_akmods_info(os_version).stage_name }},src=/rpms,dst=/tmp/rpms,rw \
{%- endif %}
--mount=type=bind,from=stage-exports,src=/exports.sh,dst=/tmp/exports.sh \
--mount=type=cache,dst=/var/cache/rpm-ostree,id=rpm-ostree-cache-{{ recipe.name }}-{{ recipe.image_version }},sharing=locked \
chmod +x /tmp/modules/{{ type }}/{{ type }}.sh \
&& source /tmp/exports.sh && /tmp/modules/{{ type }}/{{ type }}.sh '{{ module.print_module_context() }}'
{%- endif %}
{%- endif %}
{%- endfor %}