Skip to content

Commit 9db616f

Browse files
FIX mutate.Time not respecting history (#1520)
1 parent 8048663 commit 9db616f

3 files changed

Lines changed: 82 additions & 23 deletions

File tree

pkg/v1/mutate/mutate.go

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,13 @@ func inWhiteoutDir(fileMap map[string]bool, file string) bool {
332332
return false
333333
}
334334

335+
func max(a, b int) int {
336+
if a > b {
337+
return a
338+
}
339+
return b
340+
}
341+
335342
// Time sets all timestamps in an image to the given timestamp.
336343
func Time(img v1.Image, t time.Time) (v1.Image, error) {
337344
newImage := empty.Image
@@ -341,24 +348,43 @@ func Time(img v1.Image, t time.Time) (v1.Image, error) {
341348
return nil, fmt.Errorf("getting image layers: %w", err)
342349
}
343350

344-
// Strip away all timestamps from layers
345-
newLayers := make([]v1.Layer, len(layers))
346-
for idx, layer := range layers {
347-
newLayer, err := layerTime(layer, t)
351+
ocf, err := img.ConfigFile()
352+
if err != nil {
353+
return nil, fmt.Errorf("getting original config file: %w", err)
354+
}
355+
356+
addendums := make([]Addendum, max(len(ocf.History), len(layers)))
357+
var historyIdx, addendumIdx int
358+
for layerIdx := 0; layerIdx < len(layers); addendumIdx, layerIdx = addendumIdx+1, layerIdx+1 {
359+
newLayer, err := layerTime(layers[layerIdx], t)
348360
if err != nil {
349361
return nil, fmt.Errorf("setting layer times: %w", err)
350362
}
351-
newLayers[idx] = newLayer
363+
364+
// try to search for the history entry that corresponds to this layer
365+
for ; historyIdx < len(ocf.History); historyIdx++ {
366+
addendums[addendumIdx].History = ocf.History[historyIdx]
367+
// if it's an EmptyLayer, do not set the Layer and have the Addendum with just the History
368+
// and move on to the next History entry
369+
if ocf.History[historyIdx].EmptyLayer {
370+
addendumIdx++
371+
continue
372+
}
373+
// otherwise, we can exit from the cycle
374+
historyIdx++
375+
break
376+
}
377+
addendums[addendumIdx].Layer = newLayer
352378
}
353379

354-
newImage, err = AppendLayers(newImage, newLayers...)
355-
if err != nil {
356-
return nil, fmt.Errorf("appending layers: %w", err)
380+
// add all leftover History entries
381+
for ; historyIdx < len(ocf.History); historyIdx, addendumIdx = historyIdx+1, addendumIdx+1 {
382+
addendums[addendumIdx].History = ocf.History[historyIdx]
357383
}
358384

359-
ocf, err := img.ConfigFile()
385+
newImage, err = Append(newImage, addendums...)
360386
if err != nil {
361-
return nil, fmt.Errorf("getting original config file: %w", err)
387+
return nil, fmt.Errorf("appending layers: %w", err)
362388
}
363389

364390
cf, err := newImage.ConfigFile()
@@ -383,6 +409,7 @@ func Time(img v1.Image, t time.Time) (v1.Image, error) {
383409
h.Comment = ocf.History[i].Comment
384410
h.EmptyLayer = ocf.History[i].EmptyLayer
385411
// Explicitly ignore Author field; which hinders reproducibility
412+
h.Author = ""
386413
cfg.History[i] = h
387414
}
388415

pkg/v1/mutate/mutate_test.go

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"time"
2828

2929
"github.com/google/go-cmp/cmp"
30+
"github.com/google/go-cmp/cmp/cmpopts"
3031
v1 "github.com/google/go-containerregistry/pkg/v1"
3132
"github.com/google/go-containerregistry/pkg/v1/empty"
3233
"github.com/google/go-containerregistry/pkg/v1/match"
@@ -324,20 +325,47 @@ func TestMutateCreatedAt(t *testing.T) {
324325
}
325326

326327
func TestMutateTime(t *testing.T) {
327-
source := sourceImage(t)
328-
want := time.Time{}
329-
result, err := mutate.Time(source, want)
330-
if err != nil {
331-
t.Fatalf("failed to mutate a config: %v", err)
332-
}
328+
for _, tc := range []struct {
329+
name string
330+
source v1.Image
331+
}{
332+
{
333+
name: "image with matching history and layers",
334+
source: sourceImage(t),
335+
},
336+
{
337+
name: "image with empty_layer history entries",
338+
source: sourceImagePath(t, "testdata/source_image_with_empty_layer_history.tar"),
339+
},
340+
} {
341+
t.Run(tc.name, func(t *testing.T) {
342+
want := time.Time{}
343+
result, err := mutate.Time(tc.source, want)
344+
if err != nil {
345+
t.Fatalf("failed to mutate a config: %v", err)
346+
}
333347

334-
if configDigestsAreEqual(t, source, result) {
335-
t.Fatal("mutating the created time MUST mutate the config digest")
336-
}
348+
if configDigestsAreEqual(t, tc.source, result) {
349+
t.Fatal("mutating the created time MUST mutate the config digest")
350+
}
337351

338-
got := getConfigFile(t, result).Created.Time
339-
if got != want {
340-
t.Fatalf("mutating the created time MUST mutate the time from %v to %v", got, want)
352+
mutatedOriginalConfig := getConfigFile(t, tc.source).DeepCopy()
353+
gotConfig := getConfigFile(t, result)
354+
355+
// manually change the fields we expect to be changed by mutate.Time
356+
mutatedOriginalConfig.Author = ""
357+
mutatedOriginalConfig.Created = v1.Time{Time: want}
358+
for i := range mutatedOriginalConfig.History {
359+
mutatedOriginalConfig.History[i].Created = v1.Time{Time: want}
360+
mutatedOriginalConfig.History[i].Author = ""
361+
}
362+
363+
if diff := cmp.Diff(mutatedOriginalConfig, gotConfig,
364+
cmpopts.IgnoreFields(v1.RootFS{}, "DiffIDs"),
365+
); diff != "" {
366+
t.Errorf("configFile() mismatch (-want +got):\n%s", diff)
367+
}
368+
})
341369
}
342370
}
343371

@@ -636,9 +664,13 @@ func assertMTime(t *testing.T, layer v1.Layer, expectedTime time.Time) {
636664
}
637665

638666
func sourceImage(t *testing.T) v1.Image {
667+
return sourceImagePath(t, "testdata/source_image.tar")
668+
}
669+
670+
func sourceImagePath(t *testing.T, tarPath string) v1.Image {
639671
t.Helper()
640672

641-
image, err := tarball.ImageFromPath("testdata/source_image.tar", nil)
673+
image, err := tarball.ImageFromPath(tarPath, nil)
642674
if err != nil {
643675
t.Fatalf("Error loading image: %v", err)
644676
}
Binary file not shown.

0 commit comments

Comments
 (0)