mirror of
https://github.com/containers/podman.git
synced 2025-10-09 23:15:39 +08:00
Merge pull request #27155 from rhatdan/artifact
Add creation timestamp to podman artifacts
This commit is contained in:
@ -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
|
||||
|
||||
|
@ -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**
|
||||
|
@ -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)
|
||||
|
95
test/e2e/artifact_created_test.go
Normal file
95
test/e2e/artifact_created_test.go
Normal 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))
|
||||
})
|
||||
})
|
@ -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 {
|
||||
|
91
test/system/702-artifact-created.bats
Normal file
91
test/system/702-artifact-created.bats
Normal 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
|
Reference in New Issue
Block a user