Skip to content

Commit

Permalink
image/save: set a stable timestamp for assets
Browse files Browse the repository at this point in the history
When saving a docker image with `docker save`, output may have the
current timestamp, resulting in slightly changed content each time the
`save` command gets run. This patch attemtps to stabilize that effort to
clean up some spots where we've missed setting the timestamps.

It's not totally clear that setting these timestamps to 0 is the correct
behavior but it will fix the hash stability problem on output.

Signed-off-by: Stephen Day <[email protected]>
Signed-off-by: Sebastiaan van Stijn <[email protected]>
  • Loading branch information
stevvooe authored and thaJeztah committed Jan 8, 2025
1 parent e324df3 commit 392d33c
Showing 1 changed file with 36 additions and 26 deletions.
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 392d33c

Please sign in to comment.