Add creation timestamp to podman artifacts

This commit implements automatic creation timestamp functionality for artifacts
as requested in GitHub issue #27081, allowing users to see when artifacts were created.

Changes made:
- Add org.opencontainers.image.created annotation with Unix nanoseconds timestamp during artifact creation
- Preserve original creation timestamp when using --append option
- Update artifact inspect and add man pages to document the new functionality
- Add comprehensive e2e and system BATS tests to verify creation timestamp behavior
- Store timestamp as integer (Unix nanoseconds) for programmatic access

The creation timestamp helps users understand artifact freshness, particularly
useful for AI models and other time-sensitive artifacts managed by tools like RamaLama.

Usage examples:
  podman artifact add myartifact:latest /path/to/file     # Creates with timestamp
  podman artifact inspect myartifact:latest              # Shows created annotation as integer
  podman artifact add --append myartifact:latest /file2  # Preserves original timestamp

Fixes: https://github.com/containers/podman/issues/27081
Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>
This commit is contained in:
Daniel J Walsh
2025-09-24 14:04:27 -04:00
parent af65d46476
commit 4764b0e403
6 changed files with 243 additions and 3 deletions

View File

@ -0,0 +1,95 @@
//go:build linux || freebsd
package integration
import (
"os"
"path/filepath"
"time"
. "github.com/containers/podman/v5/test/utils"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Podman artifact created timestamp", func() {
createArtifactFile := func(size int) (string, error) {
artifactFile := filepath.Join(podmanTest.TempDir, RandomString(12))
f, err := os.Create(artifactFile)
if err != nil {
return "", err
}
defer f.Close()
data := RandomString(size)
_, err = f.WriteString(data)
if err != nil {
return "", err
}
return artifactFile, nil
}
It("podman artifact inspect shows created date in RFC3339 format", func() {
artifactFile, err := createArtifactFile(1024)
Expect(err).ToNot(HaveOccurred())
artifactName := "localhost/test/artifact-created"
// Record time before creation (with some buffer for slow systems)
beforeCreate := time.Now().UTC().Add(-time.Second)
// Add artifact
podmanTest.PodmanExitCleanly("artifact", "add", artifactName, artifactFile)
// Record time after creation
afterCreate := time.Now().UTC().Add(time.Second)
// Inspect artifact
a := podmanTest.InspectArtifact(artifactName)
Expect(a.Name).To(Equal(artifactName))
// Check that created annotation exists and is in valid RFC3339 format
createdStr, exists := a.Manifest.Annotations["org.opencontainers.image.created"]
Expect(exists).To(BeTrue(), "Should have org.opencontainers.image.created annotation")
// Parse the created timestamp as RFC3339Nano
createdTime, err := time.Parse(time.RFC3339Nano, createdStr)
Expect(err).ToNot(HaveOccurred(), "Created timestamp should be valid RFC3339Nano format")
// Verify timestamp is reasonable (within our time window)
Expect(createdTime).To(BeTemporally(">=", beforeCreate))
Expect(createdTime).To(BeTemporally("<=", afterCreate))
})
It("podman artifact append preserves original created date", func() {
artifactFile1, err := createArtifactFile(1024)
Expect(err).ToNot(HaveOccurred())
artifactFile2, err := createArtifactFile(2048)
Expect(err).ToNot(HaveOccurred())
artifactName := "localhost/test/artifact-append"
// Add initial artifact
podmanTest.PodmanExitCleanly("artifact", "add", artifactName, artifactFile1)
// Get initial created timestamp
a := podmanTest.InspectArtifact(artifactName)
originalCreated := a.Manifest.Annotations["org.opencontainers.image.created"]
Expect(originalCreated).ToNot(BeEmpty())
// Wait a moment to ensure timestamps would be different
time.Sleep(100 * time.Millisecond)
// Append to the artifact
podmanTest.PodmanExitCleanly("artifact", "add", "--append", artifactName, artifactFile2)
// Check that created timestamp is unchanged
a = podmanTest.InspectArtifact(artifactName)
currentCreated := a.Manifest.Annotations["org.opencontainers.image.created"]
Expect(currentCreated).To(Equal(originalCreated), "Created timestamp should not change when appending")
// Verify we have 2 layers
Expect(a.Manifest.Layers).To(HaveLen(2))
})
})

View File

@ -8,6 +8,7 @@ import (
"path/filepath"
"strconv"
"strings"
"time"
. "github.com/containers/podman/v5/test/utils"
"github.com/containers/podman/v5/utils"
@ -573,6 +574,41 @@ var _ = Describe("Podman artifact", func() {
failSession.WaitWithDefaultTimeout()
Expect(failSession).Should(ExitWithError(125, "Error: append option is not compatible with type option"))
})
It("podman artifact inspect shows created date", func() {
artifact1File, err := createArtifactFile(1024)
Expect(err).ToNot(HaveOccurred())
artifact2File, err := createArtifactFile(2048)
Expect(err).ToNot(HaveOccurred())
artifact1Name := "localhost/test/artifact1"
// Add artifact
podmanTest.PodmanExitCleanly("artifact", "add", artifact1Name, artifact1File)
// Inspect artifact
a := podmanTest.InspectArtifact(artifact1Name)
Expect(a.Name).To(Equal(artifact1Name))
// Check that created annotation exists and is in valid Unix nanosecond format
createdStr, exists := a.Manifest.Annotations["org.opencontainers.image.created"]
Expect(exists).To(BeTrue(), "Should have org.opencontainers.image.created annotation")
// podman artifact append preserves original created date
// Wait a moment to ensure timestamps would be different
time.Sleep(100 * time.Millisecond)
// Append to the artifact
podmanTest.PodmanExitCleanly("artifact", "add", "--append", artifact1Name, artifact2File)
// Check that created timestamp is unchanged
a = podmanTest.InspectArtifact(artifact1Name)
currentCreated := a.Manifest.Annotations["org.opencontainers.image.created"]
Expect(currentCreated).To(Equal(createdStr), "Created timestamp should not change when appending")
// Verify we have 2 layers
Expect(a.Manifest.Layers).To(HaveLen(2))
})
})
func digestToFilename(digest string) string {

View File

@ -0,0 +1,91 @@
#!/usr/bin/env bats -*- bats -*-
#
# Tests for podman artifact created date functionality
#
load helpers
# Create temporary artifact file for testing
function create_test_file() {
local content="$1"
local filename=$(random_string 12)
local filepath="$PODMAN_TMPDIR/$filename.txt"
echo "$content" > "$filepath"
echo "$filepath"
}
function setup() {
basic_setup
skip_if_remote "artifacts are not remote"
}
function teardown() {
run_podman artifact rm --all --ignore || true
basic_teardown
}
@test "podman artifact inspect shows created date in RFC3339 format" {
local content="test content for created date"
local testfile1=$(create_test_file "$content")
local artifact_name="localhost/test/created-test"
local content2="appended content"
local testfile2=$(create_test_file "$content2")
# Record time before creation (in seconds for comparison)
local before_epoch=$(date +%s)
# Create artifact
run_podman artifact add $artifact_name "$testfile1"
# Record time after creation (in seconds for comparison)
local after_epoch=$(date +%s)
after_epoch=$((after_epoch + 1))
# Inspect the artifact
run_podman artifact inspect $artifact_name
local output="$output"
# Parse the JSON output to get the created annotation
local created_annotation
created_annotation=$(echo "$output" | jq -r '.Manifest.annotations["org.opencontainers.image.created"]')
# Verify created annotation exists and is not null
assert "$created_annotation" != "null" "Should have org.opencontainers.image.created annotation"
assert "$created_annotation" != "" "Created annotation should not be empty"
# Verify it's a valid RFC3339 timestamp by trying to parse it
# Convert to epoch for comparison
local created_epoch
created_epoch=$(date -d "$created_annotation" +%s 2>/dev/null)
# Verify parsing succeeded
assert "$?" -eq 0 "Created timestamp should be valid RFC3339 format"
# Verify timestamp is within reasonable bounds
assert "$created_epoch" -ge "$before_epoch" "Created time should be after before_epoch"
assert "$created_epoch" -le "$after_epoch" "Created time should be before after_epoch"
# Wait a bit to ensure timestamps would differ if created new
sleep 1
# Append to artifact
run_podman artifact add --append $artifact_name "$testfile2"
# Get the created timestamp after append
run_podman artifact inspect $artifact_name
local current_created
current_created=$(echo "$output" | jq -r '.Manifest.annotations["org.opencontainers.image.created"]')
# Verify the created timestamp is preserved
assert "$current_created" = "$created_annotation" "Created timestamp should be preserved during append"
# Verify we have 2 layers now
local layer_count
layer_count=$(echo "$output" | jq '.Manifest.layers | length')
assert "$layer_count" -eq 2 "Should have 2 layers after append"
# Clean up
rm -f "$testfile1" "$testfile2"
}
# vim: filetype=sh