Skip to content

Commit 2102673

Browse files
Merge pull request #27155 from rhatdan/artifact
Add creation timestamp to podman artifacts
2 parents 8b86d14 + 4764b0e commit 2102673

File tree

6 files changed

+243
-3
lines changed

6 files changed

+243
-3
lines changed

docs/source/markdown/podman-artifact-add.1.md.in

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ Add an OCI artifact to the local store from the local filesystem. You must
1212
provide at least one file to create the artifact, but several can also be
1313
added.
1414

15+
Artifacts automatically include a creation timestamp in the
16+
`org.opencontainers.image.created` annotation using RFC3339Nano format. When using
17+
the `--append` option, the original creation timestamp is preserved.
18+
1519

1620
## OPTIONS
1721

docs/source/markdown/podman-artifact-inspect.1.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@ podman\-artifact\-inspect - Inspect an OCI artifact
88

99
## DESCRIPTION
1010

11-
Inspect an artifact in the local store. The artifact can be referred to with either:
11+
Inspect an artifact in the local store and output the results in JSON format.
12+
The artifact can be referred to with either:
1213

1314
1. Fully qualified artifact name
1415
2. Full or partial digest of the artifact's manifest
1516

17+
The inspect output includes the artifact manifest with annotations. All artifacts
18+
automatically include a creation timestamp in the `org.opencontainers.image.created`
19+
annotation using RFC3339Nano format, showing when the artifact was initially created.
20+
1621
## OPTIONS
1722

1823
#### **--help**

pkg/libartifact/store/store.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,13 +253,22 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, artifactBlobs []en
253253
if err == nil {
254254
return nil, fmt.Errorf("%s: %w", dest, libartTypes.ErrArtifactAlreadyExists)
255255
}
256+
257+
// Set creation timestamp and other annotations
258+
annotations := make(map[string]string)
259+
if options.Annotations != nil {
260+
annotations = maps.Clone(options.Annotations)
261+
}
262+
annotations[specV1.AnnotationCreated] = time.Now().UTC().Format(time.RFC3339Nano)
263+
256264
artifactManifest = specV1.Manifest{
257265
Versioned: specs.Versioned{SchemaVersion: ManifestSchemaVersion},
258266
MediaType: specV1.MediaTypeImageManifest,
259267
ArtifactType: options.ArtifactMIMEType,
260268
// TODO This should probably be configurable once the CLI is capable
261-
Config: specV1.DescriptorEmptyJSON,
262-
Layers: make([]specV1.Descriptor, 0),
269+
Config: specV1.DescriptorEmptyJSON,
270+
Layers: make([]specV1.Descriptor, 0),
271+
Annotations: annotations,
263272
}
264273
} else {
265274
artifact, _, err := artifacts.GetByNameOrDigest(dest)

test/e2e/artifact_created_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//go:build linux || freebsd
2+
3+
package integration
4+
5+
import (
6+
"os"
7+
"path/filepath"
8+
"time"
9+
10+
. "github.com/containers/podman/v5/test/utils"
11+
. "github.com/onsi/ginkgo/v2"
12+
. "github.com/onsi/gomega"
13+
)
14+
15+
var _ = Describe("Podman artifact created timestamp", func() {
16+
17+
createArtifactFile := func(size int) (string, error) {
18+
artifactFile := filepath.Join(podmanTest.TempDir, RandomString(12))
19+
f, err := os.Create(artifactFile)
20+
if err != nil {
21+
return "", err
22+
}
23+
defer f.Close()
24+
25+
data := RandomString(size)
26+
_, err = f.WriteString(data)
27+
if err != nil {
28+
return "", err
29+
}
30+
return artifactFile, nil
31+
}
32+
33+
It("podman artifact inspect shows created date in RFC3339 format", func() {
34+
artifactFile, err := createArtifactFile(1024)
35+
Expect(err).ToNot(HaveOccurred())
36+
37+
artifactName := "localhost/test/artifact-created"
38+
39+
// Record time before creation (with some buffer for slow systems)
40+
beforeCreate := time.Now().UTC().Add(-time.Second)
41+
42+
// Add artifact
43+
podmanTest.PodmanExitCleanly("artifact", "add", artifactName, artifactFile)
44+
45+
// Record time after creation
46+
afterCreate := time.Now().UTC().Add(time.Second)
47+
48+
// Inspect artifact
49+
a := podmanTest.InspectArtifact(artifactName)
50+
Expect(a.Name).To(Equal(artifactName))
51+
52+
// Check that created annotation exists and is in valid RFC3339 format
53+
createdStr, exists := a.Manifest.Annotations["org.opencontainers.image.created"]
54+
Expect(exists).To(BeTrue(), "Should have org.opencontainers.image.created annotation")
55+
56+
// Parse the created timestamp as RFC3339Nano
57+
createdTime, err := time.Parse(time.RFC3339Nano, createdStr)
58+
Expect(err).ToNot(HaveOccurred(), "Created timestamp should be valid RFC3339Nano format")
59+
60+
// Verify timestamp is reasonable (within our time window)
61+
Expect(createdTime).To(BeTemporally(">=", beforeCreate))
62+
Expect(createdTime).To(BeTemporally("<=", afterCreate))
63+
})
64+
65+
It("podman artifact append preserves original created date", func() {
66+
artifactFile1, err := createArtifactFile(1024)
67+
Expect(err).ToNot(HaveOccurred())
68+
artifactFile2, err := createArtifactFile(2048)
69+
Expect(err).ToNot(HaveOccurred())
70+
71+
artifactName := "localhost/test/artifact-append"
72+
73+
// Add initial artifact
74+
podmanTest.PodmanExitCleanly("artifact", "add", artifactName, artifactFile1)
75+
76+
// Get initial created timestamp
77+
a := podmanTest.InspectArtifact(artifactName)
78+
originalCreated := a.Manifest.Annotations["org.opencontainers.image.created"]
79+
Expect(originalCreated).ToNot(BeEmpty())
80+
81+
// Wait a moment to ensure timestamps would be different
82+
time.Sleep(100 * time.Millisecond)
83+
84+
// Append to the artifact
85+
podmanTest.PodmanExitCleanly("artifact", "add", "--append", artifactName, artifactFile2)
86+
87+
// Check that created timestamp is unchanged
88+
a = podmanTest.InspectArtifact(artifactName)
89+
currentCreated := a.Manifest.Annotations["org.opencontainers.image.created"]
90+
Expect(currentCreated).To(Equal(originalCreated), "Created timestamp should not change when appending")
91+
92+
// Verify we have 2 layers
93+
Expect(a.Manifest.Layers).To(HaveLen(2))
94+
})
95+
})

test/e2e/artifact_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"path/filepath"
99
"strconv"
1010
"strings"
11+
"time"
1112

1213
. "github.com/containers/podman/v5/test/utils"
1314
"github.com/containers/podman/v5/utils"
@@ -573,6 +574,41 @@ var _ = Describe("Podman artifact", func() {
573574
failSession.WaitWithDefaultTimeout()
574575
Expect(failSession).Should(ExitWithError(125, "Error: append option is not compatible with type option"))
575576
})
577+
578+
It("podman artifact inspect shows created date", func() {
579+
artifact1File, err := createArtifactFile(1024)
580+
Expect(err).ToNot(HaveOccurred())
581+
artifact2File, err := createArtifactFile(2048)
582+
Expect(err).ToNot(HaveOccurred())
583+
584+
artifact1Name := "localhost/test/artifact1"
585+
586+
// Add artifact
587+
podmanTest.PodmanExitCleanly("artifact", "add", artifact1Name, artifact1File)
588+
589+
// Inspect artifact
590+
a := podmanTest.InspectArtifact(artifact1Name)
591+
Expect(a.Name).To(Equal(artifact1Name))
592+
593+
// Check that created annotation exists and is in valid Unix nanosecond format
594+
createdStr, exists := a.Manifest.Annotations["org.opencontainers.image.created"]
595+
Expect(exists).To(BeTrue(), "Should have org.opencontainers.image.created annotation")
596+
597+
// podman artifact append preserves original created date
598+
// Wait a moment to ensure timestamps would be different
599+
time.Sleep(100 * time.Millisecond)
600+
601+
// Append to the artifact
602+
podmanTest.PodmanExitCleanly("artifact", "add", "--append", artifact1Name, artifact2File)
603+
604+
// Check that created timestamp is unchanged
605+
a = podmanTest.InspectArtifact(artifact1Name)
606+
currentCreated := a.Manifest.Annotations["org.opencontainers.image.created"]
607+
Expect(currentCreated).To(Equal(createdStr), "Created timestamp should not change when appending")
608+
609+
// Verify we have 2 layers
610+
Expect(a.Manifest.Layers).To(HaveLen(2))
611+
})
576612
})
577613

578614
func digestToFilename(digest string) string {
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env bats -*- bats -*-
2+
#
3+
# Tests for podman artifact created date functionality
4+
#
5+
6+
load helpers
7+
8+
# Create temporary artifact file for testing
9+
function create_test_file() {
10+
local content="$1"
11+
local filename=$(random_string 12)
12+
local filepath="$PODMAN_TMPDIR/$filename.txt"
13+
echo "$content" > "$filepath"
14+
echo "$filepath"
15+
}
16+
17+
function setup() {
18+
basic_setup
19+
skip_if_remote "artifacts are not remote"
20+
}
21+
22+
function teardown() {
23+
run_podman artifact rm --all --ignore || true
24+
basic_teardown
25+
}
26+
27+
@test "podman artifact inspect shows created date in RFC3339 format" {
28+
local content="test content for created date"
29+
local testfile1=$(create_test_file "$content")
30+
local artifact_name="localhost/test/created-test"
31+
local content2="appended content"
32+
local testfile2=$(create_test_file "$content2")
33+
34+
# Record time before creation (in seconds for comparison)
35+
local before_epoch=$(date +%s)
36+
37+
# Create artifact
38+
run_podman artifact add $artifact_name "$testfile1"
39+
40+
# Record time after creation (in seconds for comparison)
41+
local after_epoch=$(date +%s)
42+
after_epoch=$((after_epoch + 1))
43+
44+
# Inspect the artifact
45+
run_podman artifact inspect $artifact_name
46+
local output="$output"
47+
48+
# Parse the JSON output to get the created annotation
49+
local created_annotation
50+
created_annotation=$(echo "$output" | jq -r '.Manifest.annotations["org.opencontainers.image.created"]')
51+
52+
# Verify created annotation exists and is not null
53+
assert "$created_annotation" != "null" "Should have org.opencontainers.image.created annotation"
54+
assert "$created_annotation" != "" "Created annotation should not be empty"
55+
56+
# Verify it's a valid RFC3339 timestamp by trying to parse it
57+
# Convert to epoch for comparison
58+
local created_epoch
59+
created_epoch=$(date -d "$created_annotation" +%s 2>/dev/null)
60+
61+
# Verify parsing succeeded
62+
assert "$?" -eq 0 "Created timestamp should be valid RFC3339 format"
63+
64+
# Verify timestamp is within reasonable bounds
65+
assert "$created_epoch" -ge "$before_epoch" "Created time should be after before_epoch"
66+
assert "$created_epoch" -le "$after_epoch" "Created time should be before after_epoch"
67+
68+
# Wait a bit to ensure timestamps would differ if created new
69+
sleep 1
70+
71+
# Append to artifact
72+
run_podman artifact add --append $artifact_name "$testfile2"
73+
74+
# Get the created timestamp after append
75+
run_podman artifact inspect $artifact_name
76+
local current_created
77+
current_created=$(echo "$output" | jq -r '.Manifest.annotations["org.opencontainers.image.created"]')
78+
79+
# Verify the created timestamp is preserved
80+
assert "$current_created" = "$created_annotation" "Created timestamp should be preserved during append"
81+
82+
# Verify we have 2 layers now
83+
local layer_count
84+
layer_count=$(echo "$output" | jq '.Manifest.layers | length')
85+
assert "$layer_count" -eq 2 "Should have 2 layers after append"
86+
87+
# Clean up
88+
rm -f "$testfile1" "$testfile2"
89+
}
90+
91+
# vim: filetype=sh

0 commit comments

Comments
 (0)