Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
39 changes: 39 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,45 @@
COSIGN_PRIVATE_KEY: ${{ secrets.TEST_SIGNING_SECRET }}
run: just test-empty-files-build

env-expand-build:
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
contents: read # read repo contents
packages: write # write test package to ghcr
id-token: write # docker auth

steps:
- name: Maximize build space
uses: ublue-os/remove-unwanted-software@cc0becac701cf642c8f0a6613bbdaf5dc36b259e # v9

- uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
with:
install: true

- uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4

# Setup repo and add caching
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
fetch-depth: 0
ref: ${{ inputs.ref }}
repository: ${{ inputs.repo }}


- uses: extractions/setup-just@f8a3cce218d9f83db3a2ecd90e41ac3de6cdfd9b # v3

- name: Run Build
env:
GH_TOKEN: ${{ github.token }}
GH_PR_EVENT_NUMBER: ${{ inputs.pr_event_number }}
COSIGN_PRIVATE_KEY: ${{ secrets.TEST_SIGNING_SECRET }}

Check warning

Code scanning / zizmor

secrets referenced without a dedicated environment Warning test

secrets referenced without a dedicated environment
run: just test-env-expansion-build

bluefin-build:
timeout-minutes: 60
runs-on: ubuntu-latest
Expand Down
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions integration-tests/test-repo/recipes/recipe-env-expansion.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
# yaml-language-server: $schema=https://schema.blue-build.org/recipe-v1.json
name: cli/test-${NAME_EXT:-env-expansion}
description: This is my personal OS image. ${DESC_EXT}
base-image: ${BASE_REGISTRY:-quay.io}/fedora/fedora-bootc
blue-build-tag: none
cosign-version: none
image-version: ${VERSION}
stages:
- from-file: stages.yml
modules:
- from-file: common.yml
12 changes: 12 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,18 @@ test-empty-files-build: generate-test-secret install-debug-all-features
{{ should_push }} \
-vv

test-env-expansion-build: generate-test-secret install-debug-all-features
cd integration-tests/test-repo \
&& DESC_EXT="Test description" \
VERSION="43" \
bluebuild build \
--retry-push \
-B docker \
-S sigstore \
{{ should_push }} \
-vv \
recipes/recipe-env-expansion.yml

test-bluefin-build: generate-test-secret install-debug-all-features
cd integration-tests/test-repo \
&& bluebuild build \
Expand Down
12 changes: 7 additions & 5 deletions recipe/src/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use std::{
};

use blue_build_utils::{
constants::COSIGN_IMAGE_VERSION, container::Tag, platform::Platform, secret::Secret,
constants::COSIGN_IMAGE_VERSION, container::Tag, env_str::EnvString, platform::Platform,
secret::Secret,
};
use bon::Builder;
use cached::proc_macro::cached;
Expand All @@ -24,21 +25,22 @@ use crate::{Module, ModuleExt, StagesExt, maybe_version::MaybeVersion};
/// base image to assist with building the Containerfile
/// and tagging the image appropriately.
#[derive(Default, Serialize, Clone, Deserialize, Debug, Builder)]
#[builder(on(String, into))]
#[allow(clippy::duplicated_attributes)]
#[builder(on(EnvString, into), on(String, into))]
pub struct Recipe {
/// The name of the user's image.
///
/// This will be set on the `org.opencontainers.image.title` label.
pub name: String,
pub name: EnvString,

/// The description of the user's image.
///
/// This will be set on the `org.opencontainers.image.description` label.
pub description: String,
pub description: EnvString,

/// The base image from which to build the user's image.
#[serde(alias = "base-image")]
pub base_image: String,
pub base_image: EnvString,

/// The version/tag of the base image.
#[serde(alias = "image-version")]
Expand Down
4 changes: 2 additions & 2 deletions src/commands/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,11 +249,11 @@ pub fn generate_default_labels(recipe: &Recipe) -> Result<BTreeMap<String, Strin
),
(
"org.opencontainers.image.title".to_string(),
recipe.name.clone(),
recipe.name.to_string(),
),
(
"org.opencontainers.image.description".to_string(),
recipe.description.clone(),
recipe.description.to_string(),
),
("org.opencontainers.image.source".to_string(), source),
(
Expand Down
1 change: 1 addition & 0 deletions utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ constcat = "0.6.1"
directories = "6.0.0"
docker_credential = "1.3.2"
process_control = { version = "4.2.2", features = ["crossbeam-channel"] }
shellexpand = "3.1.2"

bon.workspace = true
cached.workspace = true
Expand Down
28 changes: 17 additions & 11 deletions utils/src/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use miette::miette;
use oci_client::Reference;
use serde::{Deserialize, Serialize};

use crate::platform::Platform;
use crate::{env_str::EnvString, platform::Platform};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContainerId(pub String);
Expand Down Expand Up @@ -274,7 +274,7 @@ impl PartialEq<Reference> for ImageRef<'_> {
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Tag(String);
pub struct Tag(EnvString);

impl Tag {
#[must_use]
Expand All @@ -283,15 +283,18 @@ impl Tag {
}
}

fn eval_tag(haystack: &EnvString) -> bool {
regex!(r"[\w][\w.-]{0,127}").is_match(haystack)
}

impl FromStr for Tag {
type Err = miette::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let regex = regex!(r"[\w][\w.-]{0,127}");
regex
.is_match(s)
.then(|| Self(s.into()))
.ok_or_else(|| miette!("Invalid tag: {s}"))
let expanded = EnvString::from(s);
eval_tag(&expanded)
.then(|| Self(expanded.clone()))
.ok_or_else(|| miette!("Invalid tag: {expanded}"))
}
}

Expand All @@ -317,24 +320,27 @@ impl<'de> Deserialize<'de> for Tag {
where
D: serde::Deserializer<'de>,
{
Self::from_str(&String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
let expanded = EnvString::deserialize(deserializer)?;
eval_tag(&expanded)
.then(|| Self(expanded.clone()))
.ok_or_else(|| serde::de::Error::custom(format!("Invalid tag: {expanded}")))
}
}

impl Default for Tag {
fn default() -> Self {
Self(String::from("latest"))
Self(String::from("latest").into())
}
}

impl From<Tag> for String {
fn from(value: Tag) -> Self {
value.0
value.0.to_string()
}
}

impl From<&Tag> for String {
fn from(value: &Tag) -> Self {
value.0.clone()
value.0.to_string()
}
}
98 changes: 98 additions & 0 deletions utils/src/env_str.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Default, Eq)]
pub struct EnvString {
unexpanded: String,
expanded: String,
}

impl<'de> Deserialize<'de> for EnvString {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let unexpanded = String::deserialize(deserializer)?;
let expanded = shellexpand::env(&unexpanded)
.map_err(serde::de::Error::custom)?
.into();

Ok(Self {
unexpanded,
expanded,
})
}
}

impl Serialize for EnvString {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.unexpanded)
}
}

impl std::fmt::Display for EnvString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.expanded)
}
}

impl std::ops::Deref for EnvString {
type Target = str;

fn deref(&self) -> &Self::Target {
self.expanded.as_str()
}
}

impl From<String> for EnvString {
fn from(value: String) -> Self {
Self::from(value.as_str())
}
}

impl From<&String> for EnvString {
fn from(value: &String) -> Self {
Self::from(value.as_str())
}
}

/// Context function for env var expansion.
/// If the variable doesn't exist, return None to prevent expansion.
fn context(var: &str) -> Option<String> {
crate::get_env_var(var).ok()
}

impl From<&str> for EnvString {
fn from(value: &str) -> Self {
Self {
unexpanded: value.to_string(),
expanded: shellexpand::env_with_context_no_errors(&value, context).to_string(),
}
}
}

impl From<EnvString> for String {
fn from(value: EnvString) -> Self {
value.expanded
}
}

impl PartialEq for EnvString {
fn eq(&self, other: &Self) -> bool {
self.expanded.eq(&other.expanded)
}
}

impl Ord for EnvString {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.expanded.cmp(&other.expanded)
}
}

impl PartialOrd for EnvString {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
1 change: 1 addition & 0 deletions utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod command_output;
pub mod constants;
pub mod container;
pub mod credentials;
pub mod env_str;
mod macros;
pub mod platform;
pub mod secret;
Expand Down
Loading