docker/save: stable timestamp for blobs/digest dir

Writing the OCI manifest file to the blobs/digest dir will update the
directory mtime, producing a tar file containing a member with a
contemporary mtime. Exported tars for the same image will therefore have
different checksums.

Although this was previously addressed by overriding the mtime manually
to 0, this was done before the OCI manifest file was written. This
change simply moves the call to system.Chtimes to set the mtime of the
blobs/digest directory to 0 after writing the OCI manifest file.

This commit also updates the TestSaveCheckTimes integration test to
check the mtime of all members in the exported tar to ensure that all
mtime are not newer than img.Created or 0 (depending on whether the
containerd-snapshotter is disabled or enabled, respectively).

Signed-off-by: Sam Nicholls <sam.nicholls@nanoporetech.com>
This commit is contained in:
Sam Nicholls
2025-11-03 13:15:19 +00:00
parent 560dfb13c9
commit 668b546d2c
2 changed files with 20 additions and 21 deletions

View File

@@ -264,14 +264,14 @@ func (s *saveSession) save(ctx context.Context, outStream io.Writer) error {
if err := mkdirAllWithChtimes(filepath.Dir(mFile), 0o755, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
return errors.Wrap(err, "error creating blob directory")
}
if err := system.Chtimes(filepath.Dir(mFile), time.Unix(0, 0), time.Unix(0, 0)); err != nil {
return errors.Wrap(err, "error setting blob directory timestamps")
}
if err := os.WriteFile(mFile, data, 0o644); err != nil {
return errors.Wrap(err, "error writing oci manifest file")
}
if err := system.Chtimes(mFile, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
return errors.Wrap(err, "error setting blob directory timestamps")
return errors.Wrap(err, "error setting oci manifest timestamp")
}
if err := system.Chtimes(filepath.Dir(mFile), time.Unix(0, 0), time.Unix(0, 0)); err != nil {
return errors.Wrap(err, "error setting blob digest directory timestamp")
}
untaggedMfstDesc := ocispec.Descriptor{

View File

@@ -65,26 +65,25 @@ func TestSaveCheckTimes(t *testing.T) {
rdr, err := apiClient.ImageSave(ctx, []string{repoName})
assert.NilError(t, err)
tarfs := tarIndexFS(t, rdr)
dt, err := fs.ReadFile(tarfs, "manifest.json")
assert.NilError(t, err)
var ls []imageSaveManifestEntry
assert.NilError(t, json.Unmarshal(dt, &ls))
assert.Assert(t, is.Len(ls, 1))
info, err := fs.Stat(tarfs, ls[0].Config)
assert.NilError(t, err)
created, err := time.Parse(time.RFC3339, img.Created)
assert.NilError(t, err)
if testEnv.UsingSnapshotter() {
// containerd archive export sets the mod time to zero.
assert.Check(t, is.Equal(info.ModTime(), time.Unix(0, 0)))
} else {
assert.Check(t, is.Equal(info.ModTime().Format(time.RFC3339), created.Format(time.RFC3339)))
// containerd archive export sets mod times of all members to zero
// otherwise no member should be newer than the image created date
threshold := time.Unix(0, 0)
if !testEnv.UsingSnapshotter() {
threshold = created
}
tr := tar.NewReader(rdr)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
assert.NilError(t, err)
modtime := hdr.ModTime
assert.Check(t, !modtime.After(threshold), "%s has modtime %s after %s", hdr.Name, modtime, threshold)
}
}