Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions integration-tests/test-repo/files/bind/test-file.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Test Content
53 changes: 53 additions & 0 deletions integration-tests/test-repo/recipes/common.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,56 @@ modules:
snippets:
- '[ "$(cat /tmp/test-secret)" == "TEST_PASS" ]'

# test tmpfs mounts and is writable
- type: script
mounts:
- type: tmpfs
destination: /tmp/test-tmpfs
snippets:
- '[ -d /tmp/test-tmpfs ]'
- 'echo "Hello, World!" > /tmp/test-tmpfs/hello.txt'
- '[ "$(cat /tmp/test-tmpfs/hello.txt)" == "Hello, World!" ]'

# tmpfs should no longer be available in later module runs
- type: script
snippets:
- '[ ! -d /tmp/test-tmpfs ]'

# cache mount defined at recipe should be available in all module runs with shared content
- type: script
snippets:
- '[ -d /var/cache/private ]'
- 'echo "Cache Test" > /var/cache/private/cache_test.txt'

- type: script
snippets:
- '[ "$(cat /var/cache/private/cache_test.txt)" == "Cache Test" ]'

# test bind mounts
- type: script
mounts:
- type: bind
source: files/bind
destination: /var/bind-test
snippets:
- '[ -d /var/bind-test ]'
- '[ "$(cat /var/bind-test/test-file.txt)" == "Test Content" ]'
# test writing to bind mount
- 'echo "New Content" > /var/bind-test/new-file.txt'
- '[ "$(cat /var/bind-test/new-file.txt)" == "New Content" ]'
- 'rm /var/bind-test/new-file.txt'

# test readonly bind mounts
- type: script
mounts:
- type: bind
source: files/bind
destination: /var/readonly-bind-test
readonly: true
snippets:
- '[ -d /var/readonly-bind-test ]'
- '[ "$(cat /var/readonly-bind-test/test-file.txt)" == "Test Content" ]'
# test that writing to readonly bind mount fails
- 'if echo "New Content" > /var/readonly-bind-test/new-file.txt 2>/dev/null; then exit 1; else exit 0; fi'


6 changes: 6 additions & 0 deletions integration-tests/test-repo/recipes/recipe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ base-image: quay.io/fedora/fedora-bootc
blue-build-tag: none
cosign-version: none
image-version: latest
mounts:
- type: cache
sharing: private
id: private-cache
destination: /var/cache/private

stages:
- from-file: stages.yml
modules:
Expand Down
2 changes: 2 additions & 0 deletions recipe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod akmods_info;
mod maybe_version;
mod module;
mod module_ext;
mod mount;
mod recipe;
mod stage;
mod stages_ext;
Expand All @@ -15,6 +16,7 @@ pub use akmods_info::*;
pub use maybe_version::*;
pub use module::*;
pub use module_ext::*;
pub use mount::*;
pub use recipe::*;
pub use stage::*;
pub use stages_ext::*;
Expand Down
6 changes: 5 additions & 1 deletion recipe/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use miette::{Result, bail};
use serde::{Deserialize, Serialize};
use serde_yaml::Value;

use crate::{AkmodsInfo, ModuleExt, base_recipe_path};
use crate::{AkmodsInfo, ModuleExt, base_recipe_path, mount::Mount};

mod type_ver;

Expand Down Expand Up @@ -39,6 +39,10 @@ pub struct ModuleRequiredFields {
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub secrets: Vec<Secret>,

#[builder(default)]
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub mounts: Vec<Mount>,

#[serde(flatten)]
#[builder(default, into)]
pub config: IndexMap<String, Value>,
Expand Down
112 changes: 112 additions & 0 deletions recipe/src/mount.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum MountCacheSharing {
/// The cache is shared between all builds.
#[serde(rename = "shared")]
Shared,

/// The cache is private to the current build.
#[serde(rename = "private")]
Private,

/// The cache is shared between builds, but only one build can use it at a time.
#[serde(rename = "locked")]
Locked,
}
impl std::fmt::Display for MountCacheSharing {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Shared => write!(f, "shared"),
Self::Private => write!(f, "private"),
Self::Locked => write!(f, "locked"),
}
}
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type")]
pub enum Mount {
/// A bind mount, which mounts a file or directory from the host.
#[serde(rename = "bind")]
Bind {
/// The source path on the host.
source: String,
/// The destination path in the container.
destination: String,
/// Whether the mount is read-only.
#[serde(default, rename = "readonly")]
readonly: bool,
},

/// A tmpfs mount, which mounts a temporary file system in memory.
#[serde(rename = "tmpfs")]
Tmpfs {
/// The destination path.
destination: String,
/// The size of the tmpfs. Can be specified in bytes or with a suffix (e.g. "100m" for 100 megabytes).
#[serde(skip_serializing_if = "Option::is_none")]
size: Option<String>,
},

/// A cache mount, which mounts a cache directory that can be shared between builds.
#[serde(rename = "cache")]
Cache {
/// The destination path.
destination: String,
/// The cache ID, which is used to identify the cache.
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<String>,
/// The cache sharing mode.
#[serde(skip_serializing_if = "Option::is_none")]
sharing: Option<MountCacheSharing>,
},
}

impl std::fmt::Display for Mount {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Bind {
source,
destination,
readonly,
} => {
write!(f, "type=bind,source={source},dst={destination}")?;
if *readonly {
write!(f, ",readonly")?;
}
}
Self::Tmpfs { destination, size } => {
write!(f, "type=tmpfs,dst={destination}")?;
if let Some(size) = size {
write!(f, ",size={size}")?;
}
}
Self::Cache {
destination,
id,
sharing,
} => {
write!(f, "type=cache")?;
if let Some(sharing) = sharing {
write!(f, ",sharing={sharing}")?;
}
write!(f, ",dst={destination}")?;
if let Some(id) = id {
write!(f, ",id={id}")?;
}
}
}
Ok(())
}
}

impl Mount {
#[must_use]
pub const fn oci_suffix(&self) -> &'static str {
match self {
Self::Bind { .. } => ",z",
_ => "",
}
}
}
7 changes: 6 additions & 1 deletion recipe/src/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use miette::{Context, IntoDiagnostic, Result};
use oci_client::Reference;
use serde::{Deserialize, Serialize};

use crate::{Module, ModuleExt, StagesExt, maybe_version::MaybeVersion};
use crate::{Module, ModuleExt, StagesExt, maybe_version::MaybeVersion, mount::Mount};

/// The build recipe.
///
Expand Down Expand Up @@ -90,6 +90,11 @@ pub struct Recipe {
/// This hashmap provides custom labels from ther use to the image
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<HashMap<String, String>>,

/// The mounts to add to the image.
#[serde(skip_serializing_if = "Vec::is_empty")]
Comment thread
Aex12 marked this conversation as resolved.
Outdated
#[builder(default)]
pub mounts: Vec<Mount>,
}

impl Recipe {
Expand Down
21 changes: 19 additions & 2 deletions template/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{borrow::Cow, collections::BTreeMap, fs, path::Path, process};
use std::{borrow::Cow, collections::BTreeMap, fmt::Write, fs, path::Path, process};

use blue_build_recipe::{MaybeVersion, Recipe};
use blue_build_recipe::{MaybeVersion, ModuleRequiredFields, Recipe};
use blue_build_utils::{
constants::{CONFIG_PATH, CONTAINER_FILE, CONTAINERFILES_PATH, COSIGN_PUB_PATH, FILES_PATH},
container::Tag,
Expand Down Expand Up @@ -80,6 +80,23 @@ impl ContainerFileTemplate<'_> {
}
)
}

fn user_mounts(&self, module: &ModuleRequiredFields) -> String {
let mut s = self.recipe.mounts.iter().chain(module.mounts.iter()).fold(
String::new(),
|mut acc, mount| {
let suffix: &str = match self.build_engine {
BuildEngine::Oci => mount.oci_suffix(),
BuildEngine::Docker => "",
};

writeln!(acc, "--mount={mount}{suffix} \\").unwrap();
acc
},
);
s.pop(); // Avoid trailing newline
s
}
}

#[derive(Debug, Clone, Template, Builder)]
Expand Down
3 changes: 3 additions & 0 deletions template/templates/modules/modules.j2
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ RUN \
{{ cache_mount }},dst=/var/cache/apt,id=apt-{{ cache_name }} \
{{ cache_mount }},dst=/var/cache/pacman,id=pacman-{{ cache_name }} \

{#- User defined mounts #}
{{ user_mounts(&module) }}

{#- Secret environment variables #}
{%- for secret_var in module.secrets.envs() %}
{{ secret_var }} \
Expand Down
12 changes: 12 additions & 0 deletions test-files/recipes/recipe-pass.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,15 @@ modules:
from: fedora-test
src: /test.txt
dest: /
- type: test-module
source: local
mounts:
- type: tmpfs
destination: /tmp/test
mounts:
- type: cache
id: downloads-cli-test-43
destination: /var/cache/downloads
sharing: locked
- type: tmpfs
destination: /tmp
Loading
Loading