mirror of
https://github.com/imjasonh/krust
synced 2026-07-08 06:45:32 +00:00
commit
f253f841fd
2 changed files with 124 additions and 1 deletions
|
|
@ -9,6 +9,19 @@ use std::io::Write;
|
||||||
use tar::Builder;
|
use tar::Builder;
|
||||||
use tracing::{debug, info};
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ImageConfig {
|
pub struct ImageConfig {
|
||||||
pub architecture: String,
|
pub architecture: String,
|
||||||
|
|
@ -220,7 +233,7 @@ impl ImageBuilder {
|
||||||
// Combine history (base history + app history)
|
// Combine history (base history + app history)
|
||||||
let mut merged_history = base_config.history.clone();
|
let mut merged_history = base_config.history.clone();
|
||||||
merged_history.push(History {
|
merged_history.push(History {
|
||||||
created: chrono::Utc::now().to_rfc3339(),
|
created: get_build_timestamp(),
|
||||||
created_by: "krust".to_string(),
|
created_by: "krust".to_string(),
|
||||||
comment: "Built with krust".to_string(),
|
comment: "Built with krust".to_string(),
|
||||||
empty_layer: false,
|
empty_layer: false,
|
||||||
|
|
@ -425,6 +438,45 @@ mod tests {
|
||||||
assert_eq!(parsed.history.len(), 0); // Default empty for missing field
|
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(×tamp).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(×tamp).is_ok());
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
std::env::remove_var("SOURCE_DATE_EPOCH");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_platform_invalid_format() {
|
fn test_parse_platform_invalid_format() {
|
||||||
let builder = ImageBuilder::new(
|
let builder = ImageBuilder::new(
|
||||||
|
|
|
||||||
|
|
@ -123,3 +123,74 @@ fn test_multi_arch_build_and_run() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue