Skip to content

Commit

Permalink
Merge pull request moby#48611 from stevvooe/sjd/stable-save-timestamps
Browse files Browse the repository at this point in the history
image/save: set a stable timestamp for assets
  • Loading branch information
thaJeztah authored Jan 8, 2025
2 parents 53287e4 + 392d33c commit bda51fe
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 26 deletions.
74 changes: 74 additions & 0 deletions image/tarexport/os_path.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Code in this file is a modified version of go stdlib;
// https://cs.opensource.google/go/go/+/refs/tags/go1.23.4:src/os/path.go;l=19-66

package tarexport

import (
"fmt"
"os"
"path/filepath"
"syscall"
"time"

"github.com/docker/docker/pkg/system"
)

// mkdirAllWithChtimes is nearly an identical copy to the [os.MkdirAll] but
// tracks created directories and applies the provided mtime and atime using
// [system.Chtimes].
func mkdirAllWithChtimes(path string, perm os.FileMode, atime, mtime time.Time) error {
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
dir, err := os.Stat(path)
if err == nil {
if dir.IsDir() {
return nil
}
return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR}
}

// Slow path: make sure parent exists and then call Mkdir for path.

// Extract the parent folder from path by first removing any trailing
// path separator and then scanning backward until finding a path
// separator or reaching the beginning of the string.
i := len(path) - 1
for i >= 0 && os.IsPathSeparator(path[i]) {
i--
}
for i >= 0 && !os.IsPathSeparator(path[i]) {
i--
}
if i < 0 {
i = 0
}

// If there is a parent directory, and it is not the volume name,
// recurse to ensure parent directory exists.
if parent := path[:i]; len(parent) > len(filepath.VolumeName(path)) {
err = mkdirAllWithChtimes(parent, perm, atime, mtime)
if err != nil {
return err
}
}

// Parent now exists; invoke Mkdir and use its result.
err = os.Mkdir(path, perm)
if err != nil {
// Handle arguments like "foo/." by
// double-checking that directory doesn't exist.
dir, err1 := os.Lstat(path)
if err1 == nil && dir.IsDir() {
return nil
}
return err
}

if err := system.Chtimes(path, atime, mtime); err != nil {
return fmt.Errorf("applying atime=%v and mtime=%v: %w", atime, mtime, err)
}
return nil
}
62 changes: 36 additions & 26 deletions image/tarexport/save.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ func (s *saveSession) save(ctx context.Context, outStream io.Writer) error {
dgst := digest.FromBytes(data)

mFile := filepath.Join(s.outDir, ocispec.ImageBlobsDir, dgst.Algorithm().String(), dgst.Encoded())
if err := os.MkdirAll(filepath.Dir(mFile), 0o755); err != nil {
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 {
Expand Down Expand Up @@ -385,6 +385,9 @@ func (s *saveSession) save(ctx context.Context, outStream io.Writer) error {
if err := os.WriteFile(idxFile, data, 0o644); err != nil {
return errors.Wrap(err, "error writing oci index file")
}
if err := system.Chtimes(idxFile, time.Unix(0, 0), time.Unix(0, 0)); err != nil {
return errors.Wrap(err, "error setting oci index file timestamps")
}

return s.writeTar(ctx, tempDir, outStream)
}
Expand Down Expand Up @@ -419,6 +422,11 @@ func (s *saveSession) saveImage(ctx context.Context, id image.ID) (_ map[layer.D
return nil, fmt.Errorf("empty export - not implemented")
}

ts := time.Unix(0, 0)
if img.Created != nil {
ts = *img.Created
}

var parent digest.Digest
var layers []layer.DiffID
var foreignSrcs map[layer.DiffID]distribution.Descriptor
Expand Down Expand Up @@ -450,7 +458,7 @@ func (s *saveSession) saveImage(ctx context.Context, id image.ID) (_ map[layer.D
}

v1Img.OS = img.OS
src, err := s.saveConfigAndLayer(ctx, rootFS.ChainID(), v1Img, img.Created)
src, err := s.saveConfigAndLayer(ctx, rootFS.ChainID(), v1Img, &ts)
if err != nil {
return nil, err
}
Expand All @@ -469,26 +477,22 @@ func (s *saveSession) saveImage(ctx context.Context, id image.ID) (_ map[layer.D
dgst := digest.FromBytes(data)

blobDir := filepath.Join(s.outDir, ocispec.ImageBlobsDir, dgst.Algorithm().String())
if err := os.MkdirAll(blobDir, 0o755); err != nil {
if err := mkdirAllWithChtimes(blobDir, 0o755, ts, ts); err != nil {
return nil, err
}
if img.Created != nil {
if err := system.Chtimes(blobDir, *img.Created, *img.Created); err != nil {
return nil, err
}
if err := system.Chtimes(filepath.Dir(blobDir), *img.Created, *img.Created); err != nil {
return nil, err
}
if err := system.Chtimes(blobDir, ts, ts); err != nil {
return nil, err
}
if err := system.Chtimes(filepath.Dir(blobDir), ts, ts); err != nil {
return nil, err
}

configFile := filepath.Join(blobDir, dgst.Encoded())
if err := os.WriteFile(configFile, img.RawJSON(), 0o644); err != nil {
return nil, err
}
if img.Created != nil {
if err := system.Chtimes(configFile, *img.Created, *img.Created); err != nil {
return nil, err
}
if err := system.Chtimes(configFile, ts, ts); err != nil {
return nil, err
}

s.images[id].layers = layers
Expand All @@ -506,6 +510,11 @@ func (s *saveSession) saveConfigAndLayer(ctx context.Context, id layer.ChainID,
span.SetStatus(outErr)
}()

ts := time.Unix(0, 0)
if createdTime != nil {
ts = *createdTime
}

outDir := filepath.Join(s.outDir, ocispec.ImageBlobsDir)

if _, ok := s.savedConfigs[legacyImg.ID]; !ok {
Expand Down Expand Up @@ -542,7 +551,7 @@ func (s *saveSession) saveConfigAndLayer(ctx context.Context, id layer.ChainID,

// We use sequential file access to avoid depleting the standby list on
// Windows. On Linux, this equates to a regular os.Create.
if err := os.MkdirAll(filepath.Dir(layerPath), 0o755); err != nil {
if err := mkdirAllWithChtimes(filepath.Dir(layerPath), 0o755, ts, ts); err != nil {
return distribution.Descriptor{}, errors.Wrap(err, "could not create layer dir parent")
}
tarFile, err := sequential.Create(layerPath)
Expand Down Expand Up @@ -576,12 +585,10 @@ func (s *saveSession) saveConfigAndLayer(ctx context.Context, id layer.ChainID,
layerPath = filepath.Join(outDir, lDgst.Algorithm().String(), lDgst.Encoded())
}

if createdTime != nil {
for _, fname := range []string{outDir, layerPath} {
// todo: maybe save layer created timestamp?
if err := system.Chtimes(fname, *createdTime, *createdTime); err != nil {
return distribution.Descriptor{}, errors.Wrap(err, "could not set layer timestamp")
}
for _, fname := range []string{outDir, layerPath} {
// todo: maybe save layer created timestamp?
if err := system.Chtimes(fname, ts, ts); err != nil {
return distribution.Descriptor{}, errors.Wrap(err, "could not set layer timestamp")
}
}

Expand All @@ -608,20 +615,23 @@ func (s *saveSession) saveConfig(legacyImg image.V1Image, outDir string, created
return err
}

ts := time.Unix(0, 0)
if createdTime != nil {
ts = *createdTime
}

cfgDgst := digest.FromBytes(imageConfig)
configPath := filepath.Join(outDir, cfgDgst.Algorithm().String(), cfgDgst.Encoded())
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
if err := mkdirAllWithChtimes(filepath.Dir(configPath), 0o755, ts, ts); err != nil {
return errors.Wrap(err, "could not create layer dir parent")
}

if err := os.WriteFile(configPath, imageConfig, 0o644); err != nil {
return err
}

if createdTime != nil {
if err := system.Chtimes(configPath, *createdTime, *createdTime); err != nil {
return errors.Wrap(err, "could not set config timestamp")
}
if err := system.Chtimes(configPath, ts, ts); err != nil {
return errors.Wrap(err, "could not set config timestamp")
}

s.savedConfigs[legacyImg.ID] = struct{}{}
Expand Down

0 comments on commit bda51fe

Please sign in to comment.