Merge pull request #27155 from rhatdan/artifact

Add creation timestamp to podman artifacts
This commit is contained in:
openshift-merge-bot[bot]
2025-09-25 15:31:16 +00:00
committed by GitHub
6 changed files with 243 additions and 3 deletions

View File

@ -12,6 +12,10 @@ Add an OCI artifact to the local store from the local filesystem. You must
provide at least one file to create the artifact, but several can also be
added.
Artifacts automatically include a creation timestamp in the
`org.opencontainers.image.created` annotation using RFC3339Nano format. When using
the `--append` option, the original creation timestamp is preserved.
## OPTIONS

View File

@ -8,11 +8,16 @@ podman\-artifact\-inspect - Inspect an OCI artifact
## DESCRIPTION
Inspect an artifact in the local store. The artifact can be referred to with either:
Inspect an artifact in the local store and output the results in JSON format.
The artifact can be referred to with either:
1. Fully qualified artifact name
2. Full or partial digest of the artifact's manifest
The inspect output includes the artifact manifest with annotations. All artifacts
automatically include a creation timestamp in the `org.opencontainers.image.created`
annotation using RFC3339Nano format, showing when the artifact was initially created.
## OPTIONS
#### **--help**

View File

@ -253,13 +253,22 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, artifactBlobs []en
if err == nil {
return nil, fmt.Errorf("%s: %w", dest, libartTypes.ErrArtifactAlreadyExists)
}
// Set creation timestamp and other annotations
annotations := make(map[string]string)
if options.Annotations != nil {
annotations = maps.Clone(options.Annotations)
}
annotations[specV1.AnnotationCreated] = time.Now().UTC().Format(time.RFC3339Nano)
artifactManifest = specV1.Manifest{
Versioned: specs.Versioned{SchemaVersion: ManifestSchemaVersion},
MediaType: specV1.MediaTypeImageManifest,
ArtifactType: options.ArtifactMIMEType,
// TODO This should probably be configurable once the CLI is capable
Config: specV1.DescriptorEmptyJSON,
Layers: make([]specV1.Descriptor, 0),
Config: specV1.DescriptorEmptyJSON,
Layers: make([]specV1.Descriptor, 0),
Annotations: annotations,
}
} else {
artifact, _, err := artifacts.GetByNameOrDigest(dest)

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