1
0
Fork 0
mirror of https://github.com/imjasonh/krust synced 2026-07-06 22:12:32 +00:00

reproducible

Signed-off-by: Jason Hall <jason@chainguard.dev>
This commit is contained in:
Jason Hall 2025-10-15 16:05:23 -04:00
parent 34eb31b29d
commit 72dc9ef103
2 changed files with 124 additions and 1 deletions

View file

@ -9,6 +9,19 @@ use std::io::Write;
use tar::Builder;
use tracing::{debug, info};
/// Get the timestamp to use for reproducible builds.
/// Respects SOURCE_DATE_EPOCH environment variable if set.
fn get_build_timestamp() -> String {
if let Ok(epoch) = std::env::var("SOURCE_DATE_EPOCH") {
if let Ok(timestamp) = epoch.parse::<i64>() {
if let Some(dt) = chrono::DateTime::from_timestamp(timestamp, 0) {
return dt.to_rfc3339();
}
}
}
chrono::Utc::now().to_rfc3339()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageConfig {
pub architecture: String,
@ -220,7 +233,7 @@ impl ImageBuilder {
// Combine history (base history + app history)
let mut merged_history = base_config.history.clone();
merged_history.push(History {
created: chrono::Utc::now().to_rfc3339(),
created: get_build_timestamp(),
created_by: "krust".to_string(),
comment: "Built with krust".to_string(),
empty_layer: false,
@ -424,4 +437,43 @@ mod tests {
assert_eq!(parsed.rootfs.diff_ids.len(), 1);
assert_eq!(parsed.history.len(), 0); // Default empty for missing field
}
#[test]
fn test_get_build_timestamp_respects_source_date_epoch() {
// Set SOURCE_DATE_EPOCH
std::env::set_var("SOURCE_DATE_EPOCH", "1609459200");
let timestamp = super::get_build_timestamp();
// Should be 2021-01-01T00:00:00+00:00
assert!(timestamp.starts_with("2021-01-01T00:00:00"));
// Clean up
std::env::remove_var("SOURCE_DATE_EPOCH");
}
#[test]
fn test_get_build_timestamp_without_source_date_epoch() {
// Make sure SOURCE_DATE_EPOCH is not set
std::env::remove_var("SOURCE_DATE_EPOCH");
let timestamp = super::get_build_timestamp();
// Should be a valid RFC3339 timestamp
assert!(chrono::DateTime::parse_from_rfc3339(&timestamp).is_ok());
}
#[test]
fn test_get_build_timestamp_invalid_epoch() {
// Set invalid SOURCE_DATE_EPOCH
std::env::set_var("SOURCE_DATE_EPOCH", "not-a-number");
let timestamp = super::get_build_timestamp();
// Should fall back to current time
assert!(chrono::DateTime::parse_from_rfc3339(&timestamp).is_ok());
// Clean up
std::env::remove_var("SOURCE_DATE_EPOCH");
}
}

View file

@ -123,3 +123,74 @@ fn test_multi_arch_build_and_run() -> Result<()> {
Ok(())
}
#[test]
fn test_reproducible_builds() -> Result<()> {
// This test verifies that building the same project twice with SOURCE_DATE_EPOCH
// produces identical image digests
// Get the example project directory
let example_dir = env::current_dir()?.join("example").join("hello-krust");
let native_platform = if cfg!(target_arch = "aarch64") {
"linux/arm64"
} else {
"linux/amd64"
};
// Fixed timestamp for reproducibility
let source_date_epoch = "1609459200"; // 2021-01-01 00:00:00 UTC
// Build the first time (pushing to ttl.sh for temporary storage)
let mut cmd1 = Command::cargo_bin("krust")?;
let output1 = cmd1
.arg("build")
.arg("--platform")
.arg(native_platform)
.arg(".")
.env("KRUST_REPO", "ttl.sh/krust-reproducible-test")
.env("SOURCE_DATE_EPOCH", source_date_epoch)
.current_dir(&example_dir)
.output()?;
if !output1.status.success() {
let stderr = String::from_utf8_lossy(&output1.stderr);
panic!("First build failed: {}", stderr);
}
let digest1 = String::from_utf8_lossy(&output1.stdout).trim().to_string();
// Build the second time with the same SOURCE_DATE_EPOCH
let mut cmd2 = Command::cargo_bin("krust")?;
let output2 = cmd2
.arg("build")
.arg("--platform")
.arg(native_platform)
.arg(".")
.env("KRUST_REPO", "ttl.sh/krust-reproducible-test")
.env("SOURCE_DATE_EPOCH", source_date_epoch)
.current_dir(&example_dir)
.output()?;
if !output2.status.success() {
let stderr = String::from_utf8_lossy(&output2.stderr);
panic!("Second build failed: {}", stderr);
}
let digest2 = String::from_utf8_lossy(&output2.stdout).trim().to_string();
// The digests should be identical
assert_eq!(
digest1, digest2,
"Builds with same SOURCE_DATE_EPOCH should produce identical digests.\nFirst: {}\nSecond: {}",
digest1, digest2
);
// Both should contain a valid digest
assert!(
digest1.contains("@sha256:"),
"Output should contain a digest"
);
Ok(())
}