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:
parent
e4a0c90bdc
commit
57e8568849
@ -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
4
go.mod
@ -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
8
go.sum
@ -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=
|
||||
|
@ -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 don’t 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.
|
||||
|
@ -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,
|
||||
// Don’t 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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user