diff --git a/image/tarexport/os_path.go b/image/tarexport/os_path.go new file mode 100644 index 0000000000000..4b289034a4a8a --- /dev/null +++ b/image/tarexport/os_path.go @@ -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 +} diff --git a/image/tarexport/save.go b/image/tarexport/save.go index 53b2acefa2b76..76b73ddade499 100644 --- a/image/tarexport/save.go +++ b/image/tarexport/save.go @@ -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 { @@ -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) } @@ -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 @@ -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 } @@ -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 @@ -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 { @@ -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) @@ -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") } } @@ -608,9 +615,14 @@ 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") } @@ -618,10 +630,8 @@ func (s *saveSession) saveConfig(legacyImg image.V1Image, outDir string, created 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{}{}