mirror of
https://github.com/coreos/chunkah.git
synced 2026-11-03 21:49:44 -09:00
build: use image Created timestamp as source date epoch fallback
When splitting an existing image and no SOURCE_DATE_EPOCH is provided, use the Created timestamp from the image inspect output instead of the current time. This makes re-splitting an existing image reproducible without requiring the caller to explicitly set SOURCE_DATE_EPOCH. Assisted-by: Claude Opus 4.6
This commit is contained in:
parent
557c974e53
commit
6905ef92ba
3 changed files with 71 additions and 20 deletions
|
|
@ -147,11 +147,6 @@ impl BuildArgs {
|
|||
pub fn run(args: &BuildArgs) -> Result<()> {
|
||||
tracing::info!(rootfs = %args.rootfs, "starting build");
|
||||
|
||||
let created_epoch = args
|
||||
.source_date_epoch
|
||||
.map_or_else(utils::get_current_epoch, Ok)?;
|
||||
tracing::debug!(created_epoch, "using timestamp");
|
||||
|
||||
// load base config from file, string, or use empty default
|
||||
let parsed = if let Some(path) = &args.config {
|
||||
tracing::debug!(source = %path, "loading config from file");
|
||||
|
|
@ -167,9 +162,12 @@ pub fn run(args: &BuildArgs) -> Result<()> {
|
|||
config: oci_image::Config::default(),
|
||||
annotations: HashMap::new(),
|
||||
architecture: None,
|
||||
created: None,
|
||||
}
|
||||
};
|
||||
|
||||
let created_epoch = resolve_created_epoch(args.source_date_epoch, &parsed)?;
|
||||
|
||||
let architecture = args.arch.as_deref().or(parsed.architecture.as_deref());
|
||||
// get the current arch if not provided, but even if provided, this
|
||||
// normalizes the arch so that `--arch x86_64` also works
|
||||
|
|
@ -252,6 +250,7 @@ fn parse_config(json_str: &str) -> Result<ParsedConfig> {
|
|||
config,
|
||||
annotations: HashMap::new(),
|
||||
architecture: None,
|
||||
created: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
@ -266,6 +265,8 @@ struct ParsedConfig {
|
|||
annotations: HashMap<String, String>,
|
||||
#[serde(rename = "Architecture")]
|
||||
architecture: Option<String>,
|
||||
#[serde(rename = "Created")]
|
||||
created: Option<String>,
|
||||
}
|
||||
|
||||
/// Config input format - either direct OCI config or podman/docker inspect output.
|
||||
|
|
@ -280,6 +281,25 @@ enum ConfigInput {
|
|||
Direct(oci_image::Config),
|
||||
}
|
||||
|
||||
/// Resolve the created epoch from CLI, config, or current time.
|
||||
///
|
||||
/// Priority: explicit `--source-date-epoch` > `Created` from image inspect > current time.
|
||||
fn resolve_created_epoch(source_date_epoch: Option<u64>, parsed: &ParsedConfig) -> Result<u64> {
|
||||
if let Some(epoch) = source_date_epoch {
|
||||
tracing::debug!(epoch, "using source date epoch from CLI/env");
|
||||
Ok(epoch)
|
||||
} else if let Some(created) = &parsed.created {
|
||||
let epoch =
|
||||
utils::parse_rfc3339_epoch(created).context("parsing image created timestamp")?;
|
||||
tracing::debug!(epoch, created = %created, "using build date from image config");
|
||||
Ok(epoch)
|
||||
} else {
|
||||
let epoch = utils::get_current_epoch()?;
|
||||
tracing::debug!(epoch, "using current time as source date epoch");
|
||||
Ok(epoch)
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the OCI image configuration from CLI args and a parsed config.
|
||||
fn build_image_config(
|
||||
args: &BuildArgs,
|
||||
|
|
@ -525,7 +545,8 @@ mod tests {
|
|||
"Annotations": {
|
||||
"org.example.key": "value"
|
||||
},
|
||||
"Architecture": "arm64"
|
||||
"Architecture": "arm64",
|
||||
"Created": "2023-11-14T22:13:20Z"
|
||||
}]"#;
|
||||
let parsed = parse_config(json).unwrap();
|
||||
|
||||
|
|
@ -542,6 +563,7 @@ mod tests {
|
|||
Some(&"value".to_string())
|
||||
);
|
||||
assert_eq!(parsed.architecture, Some("arm64".to_string()));
|
||||
assert_eq!(parsed.created, Some("2023-11-14T22:13:20Z".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
18
src/utils.rs
18
src/utils.rs
|
|
@ -9,6 +9,13 @@ pub fn get_current_epoch() -> Result<u64> {
|
|||
.map(|d| d.as_secs())
|
||||
}
|
||||
|
||||
/// Parse an RFC 3339 timestamp string into a Unix epoch (seconds).
|
||||
pub fn parse_rfc3339_epoch(s: &str) -> Result<u64> {
|
||||
let dt = chrono::DateTime::parse_from_rfc3339(s)
|
||||
.with_context(|| format!("parsing RFC 3339 timestamp: {s}"))?;
|
||||
u64::try_from(dt.timestamp()).with_context(|| format!("timestamp is negative: {s}"))
|
||||
}
|
||||
|
||||
/// Format a byte count as a human-readable string using binary units.
|
||||
pub fn format_size(bytes: u64) -> String {
|
||||
const KIB: f64 = 1024.0;
|
||||
|
|
@ -44,6 +51,17 @@ pub fn get_goarch(arch: Option<&str>) -> &str {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_rfc3339_epoch() {
|
||||
assert_eq!(
|
||||
parse_rfc3339_epoch("2023-11-14T22:13:20Z").unwrap(),
|
||||
1700000000
|
||||
);
|
||||
assert_eq!(parse_rfc3339_epoch("1970-01-01T00:00:00Z").unwrap(), 0);
|
||||
assert!(parse_rfc3339_epoch("not-a-date").is_err());
|
||||
assert!(parse_rfc3339_epoch("1969-12-31T23:59:59Z").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_size() {
|
||||
assert_eq!(format_size(0), "0 B");
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
# Test that chunked images are reproducible when built with SOURCE_DATE_EPOCH.
|
||||
# Test that chunked images are reproducible.
|
||||
set -xeuo pipefail
|
||||
shopt -s inherit_errexit
|
||||
|
||||
|
|
@ -17,23 +17,34 @@ trap cleanup EXIT
|
|||
podman pull "${SOURCE_IMAGE}"
|
||||
CHUNKAH_CONFIG_STR=$(podman inspect "${SOURCE_IMAGE}")
|
||||
|
||||
# run chunkah twice on the same rootfs with a fixed SOURCE_DATE_EPOCH
|
||||
assert_archives_identical() {
|
||||
local sum1 sum2
|
||||
sum1=$(sha256sum out1.ociarchive | cut -d' ' -f1)
|
||||
sum2=$(sha256sum out2.ociarchive | cut -d' ' -f1)
|
||||
if [[ "${sum1}" != "${sum2}" ]]; then
|
||||
echo "ERROR: OCI archives differ between builds"
|
||||
echo "Build 1: ${sum1}"
|
||||
echo "Build 2: ${sum2}"
|
||||
if command -v diffoscope &>/dev/null; then
|
||||
diffoscope out1.ociarchive out2.ociarchive || true
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 1: reproducible with explicit SOURCE_DATE_EPOCH
|
||||
for i in 1 2; do
|
||||
podman run --rm --mount=type=image,src="${SOURCE_IMAGE}",target=/chunkah \
|
||||
-e CHUNKAH_CONFIG_STR="${CHUNKAH_CONFIG_STR}" \
|
||||
-e SOURCE_DATE_EPOCH=1700000000 \
|
||||
"${CHUNKAH_IMG:?}" build -v > "out${i}.ociarchive"
|
||||
done
|
||||
assert_archives_identical
|
||||
|
||||
# the two OCI archives should be byte-identical
|
||||
sha1=$(sha256sum out1.ociarchive | cut -d' ' -f1)
|
||||
sha2=$(sha256sum out2.ociarchive | cut -d' ' -f1)
|
||||
if [[ "${sha1}" != "${sha2}" ]]; then
|
||||
echo "ERROR: OCI archives differ between builds"
|
||||
echo "Build 1: ${sha1}"
|
||||
echo "Build 2: ${sha2}"
|
||||
if command -v diffoscope &>/dev/null; then
|
||||
diffoscope out1.ociarchive out2.ociarchive || true
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
# Test 2: reproducible without SOURCE_DATE_EPOCH (uses image's Created timestamp)
|
||||
for i in 1 2; do
|
||||
podman run --rm --mount=type=image,src="${SOURCE_IMAGE}",target=/chunkah \
|
||||
-e CHUNKAH_CONFIG_STR="${CHUNKAH_CONFIG_STR}" \
|
||||
"${CHUNKAH_IMG:?}" build -v > "out${i}.ociarchive"
|
||||
done
|
||||
assert_archives_identical
|
||||
|
|
|
|||
Loading…
Reference in a new issue