1
0
mirror of https://github.com/containers/image.git synced 2025-04-18 19:44:05 +03:00

feat: add DestinationTimestamp to copy options

Useful for reproducible oci-archive output.

Signed-off-by: Adam Eijdenberg <adam@continusec.com>
This commit is contained in:
Adam Eijdenberg 2025-02-22 04:08:32 +00:00
parent e4a0c90bdc
commit 57e8568849
7 changed files with 59 additions and 12 deletions

View File

@ -148,6 +148,13 @@ type Options struct {
// so that storage.ResolveReference returns exactly the created image.
// WARNING: It is unspecified whether the reference also contains a reference.Named element.
ReportResolvedReference *types.ImageReference
// DestinationTimestamp, if set, will force timestamps of content created in the destination to this value.
// Most transports don't support this.
//
// In oci-archive: destinations, this will set the create/mod/access timestamps in each tar entry
// (but not a timestamp of the created archive file).
DestinationTimestamp *time.Time
}
// OptionCompressionVariant allows to supply information about
@ -354,6 +361,7 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef,
if err := c.dest.CommitWithOptions(ctx, private.CommitOptions{
UnparsedToplevel: c.unparsedToplevel,
ReportResolvedReference: options.ReportResolvedReference,
Timestamp: options.DestinationTimestamp,
}); err != nil {
return nil, fmt.Errorf("committing the finished image: %w", err)
}

4
go.mod
View File

@ -10,7 +10,7 @@ require (
github.com/BurntSushi/toml v1.4.0
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01
github.com/containers/ocicrypt v1.2.1
github.com/containers/storage v1.57.2-0.20250211190637-7aa96daee0a3
github.com/containers/storage v1.57.2-0.20250220203011-3a013da40ef1
github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f
github.com/distribution/reference v0.6.0
github.com/docker/cli v28.0.1+incompatible
@ -67,7 +67,7 @@ require (
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/coreos/go-oidc/v3 v3.12.0 // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-units v0.5.0 // indirect

8
go.sum
View File

@ -56,14 +56,14 @@ github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYgle
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY=
github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM=
github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ=
github.com/containers/storage v1.57.2-0.20250211190637-7aa96daee0a3 h1:YLjd5aplmRP98Jlrqz5+kNmbVZvpZwrZygkF96KR2Fs=
github.com/containers/storage v1.57.2-0.20250211190637-7aa96daee0a3/go.mod h1:zsh6czcxcdqKIz//cVU6waEJ+2Ui8OEnrwCvM/DE3iU=
github.com/containers/storage v1.57.2-0.20250220203011-3a013da40ef1 h1:Gsx/Ad+axho5kmTCshG82Ghlvle8sMDGC74tukRE9aU=
github.com/containers/storage v1.57.2-0.20250220203011-3a013da40ef1/go.mod h1:egC90qMy0fTpGjkaHj667syy1Cbr3XPZEVX/qkUPrdM=
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f h1:eHnXnuK47UlSTOQexbzxAZfekVz6i+LKRdj1CU5DPaM=
github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=

View File

@ -3,6 +3,7 @@ package private
import (
"context"
"io"
"time"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/internal/blobinfocache"
@ -170,6 +171,12 @@ type CommitOptions struct {
// What “resolved” means is transport-specific.
// Transports which dont support reporting resolved references can ignore the field; the generic copy code writes "nil" into the value.
ReportResolvedReference *types.ImageReference
// Timestamp, if set, will force timestamps of content created in the destination to this value.
// Most transports don't support this.
//
// In oci-archive: destinations, this will set the create/mod/access timestamps in each tar entry
// (but not a timestamp of the created archive file).
Timestamp *time.Time
}
// ImageSourceChunk is a portion of a blob.

View File

@ -5,6 +5,7 @@ import (
"fmt"
"io"
"os"
"time"
"github.com/containers/image/v5/internal/imagedestination"
"github.com/containers/image/v5/internal/imagedestination/impl"
@ -172,16 +173,19 @@ func (d *ociArchiveImageDestination) CommitWithOptions(ctx context.Context, opti
src := d.tempDirRef.tempDirectory
// path to save tarred up file
dst := d.ref.resolvedFile
return tarDirectory(src, dst)
return tarDirectory(src, dst, options.Timestamp)
}
// tar converts the directory at src and saves it to dst
func tarDirectory(src, dst string) error {
// if contentModTimes is non-nil, tar header entries times are set to this
func tarDirectory(src, dst string, contentModTimes *time.Time) error {
// input is a stream of bytes from the archive of the directory at path
input, err := archive.TarWithOptions(src, &archive.TarOptions{
Compression: archive.Uncompressed,
// Dont include the data about the user account this code is running under.
ChownOpts: &idtools.IDPair{UID: 0, GID: 0},
// override tar header timestamps
Timestamp: contentModTimes,
})
if err != nil {
return fmt.Errorf("retrieving stream of bytes from %q: %w", src, err)

View File

@ -20,7 +20,7 @@ func TestTarDirectory(t *testing.T) {
require.NoError(t, err)
dest := filepath.Join(t.TempDir(), "file.tar")
err = tarDirectory(srcDir, dest)
err = tarDirectory(srcDir, dest, nil)
require.NoError(t, err)
f, err := os.Open(dest)

View File

@ -1,10 +1,13 @@
package archive
import (
"archive/tar"
"context"
"io"
"os"
"path/filepath"
"testing"
"time"
_ "github.com/containers/image/v5/internal/testing/explicitfilepath-tmpdir"
"github.com/containers/image/v5/types"
@ -139,7 +142,7 @@ func refToTempOCI(t *testing.T) (types.ImageReference, string) {
// refToTempOCIArchive creates a temporary directory, copies the contents of that directory
// to a temporary tar file and returns a reference to the temporary tar file
func refToTempOCIArchive(t *testing.T) (ref types.ImageReference, tmpTarFile string) {
func refToTempOCIArchive(t *testing.T, tarEntryTimestamp *time.Time) (ref types.ImageReference, tmpTarFile string) {
tmpDir := t.TempDir()
m := `{
"schemaVersion": 2,
@ -163,7 +166,7 @@ func refToTempOCIArchive(t *testing.T) (ref types.ImageReference, tmpTarFile str
require.NoError(t, err)
tarFile, err := os.CreateTemp("", "oci-transport-test.tar")
require.NoError(t, err)
err = tarDirectory(tmpDir, tarFile.Name())
err = tarDirectory(tmpDir, tarFile.Name(), tarEntryTimestamp)
require.NoError(t, err)
ref, err = NewReference(tarFile.Name(), "")
require.NoError(t, err)
@ -253,13 +256,38 @@ func TestReferenceNewImage(t *testing.T) {
}
func TestReferenceNewImageSource(t *testing.T) {
ref, tmpTarFile := refToTempOCIArchive(t)
ref, tmpTarFile := refToTempOCIArchive(t, nil)
defer os.RemoveAll(tmpTarFile)
src, err := ref.NewImageSource(context.Background(), nil)
assert.NoError(t, err)
defer src.Close()
}
func TestTimestampEntriesPassedThrough(t *testing.T) {
// set target time to a bit in the future, but rounded
targetTime := time.Now().Add(time.Hour).Truncate(time.Second)
_, tmpTarFile := refToTempOCIArchive(t, &targetTime)
defer os.RemoveAll(tmpTarFile)
f, err := os.Open(tmpTarFile)
assert.NoError(t, err)
defer f.Close()
numEntries := 0
tr := tar.NewReader(f)
for {
th, err := tr.Next()
if err == io.EOF {
break
}
assert.NoError(t, err)
assert.Equal(t, targetTime, th.ModTime) // access time and change time are ignored by Go's tar.Writer unless the creator explicitly sets a non-default header format, so just check mod time
numEntries++
}
assert.NotEqual(t, 0, numEntries)
}
func TestReferenceNewImageDestination(t *testing.T) {
ref, _ := refToTempOCI(t)
dest, err := ref.NewImageDestination(context.Background(), nil)