diff --git a/pkg/executor/build.go b/pkg/executor/build.go index 02ce01647b..6e84ffdf14 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -115,7 +115,7 @@ func newStageBuilder(args *dockerfile.BuildArgs, opts *config.KanikoOptions, sta if !opts.Reproducible { snapshotter = snapshot.NewSnapshotter(l, config.RootDir) } else { - snapshotter = snapshot.NewCanonicalSnapshotter(l, config.RootDir) + snapshotter = snapshot.NewReproducibleSnapshotter(l, config.RootDir) } digest, err := sourceImage.Digest() diff --git a/pkg/snapshot/snapshot.go b/pkg/snapshot/snapshot.go index 450f96ec4d..fb98432521 100644 --- a/pkg/snapshot/snapshot.go +++ b/pkg/snapshot/snapshot.go @@ -39,10 +39,10 @@ var snapshotPathPrefix = "" // Snapshotter holds the root directory from which to take snapshots, and a list of snapshots taken type Snapshotter struct { - l *LayeredMap - directory string - ignorelist []util.IgnoreListEntry - canonical bool + l *LayeredMap + directory string + ignorelist []util.IgnoreListEntry + reproducible bool } // NewSnapshotter creates a new snapshotter rooted at d @@ -50,10 +50,10 @@ func NewSnapshotter(l *LayeredMap, d string) *Snapshotter { return &Snapshotter{l: l, directory: d, ignorelist: util.IgnoreList()} } -// NewCanonicalSnapshotter creates a new snapshotter rooted at d that produces +// NewReproducibleSnapshotter creates a new snapshotter rooted at d that produces // reproducible snapshots. -func NewCanonicalSnapshotter(l *LayeredMap, d string) *Snapshotter { - return &Snapshotter{l: l, directory: d, ignorelist: util.IgnoreList(), canonical: true} +func NewReproducibleSnapshotter(l *LayeredMap, d string) *Snapshotter { + return &Snapshotter{l: l, directory: d, ignorelist: util.IgnoreList(), reproducible: true} } // Init initializes a new snapshotter @@ -120,10 +120,10 @@ func (s *Snapshotter) TakeSnapshot(files []string, shdCheckDelete bool, forceBui } var t util.Tar - if !s.canonical { + if !s.reproducible { t = util.NewTar(f) } else { - t = util.NewCanonicalTar(f) + t = util.NewReproducibleTar(f) } defer t.Close() if err := writeToTar(t, filesToAdd, filesToWhiteout); err != nil { @@ -141,10 +141,10 @@ func (s *Snapshotter) TakeSnapshotFS() (string, error) { } defer f.Close() var t util.Tar - if !s.canonical { + if !s.reproducible { t = util.NewTar(f) } else { - t = util.NewCanonicalTar(f) + t = util.NewReproducibleTar(f) } defer t.Close() diff --git a/pkg/util/tar_util.go b/pkg/util/tar_util.go index 4dda4d1744..2faf9cb991 100644 --- a/pkg/util/tar_util.go +++ b/pkg/util/tar_util.go @@ -38,9 +38,9 @@ import ( // Tar knows how to write files to a tar file. type Tar struct { - hardlinks map[uint64]string - w *tar.Writer - canonical bool + hardlinks map[uint64]string + w *tar.Writer + ignoreTimestamps bool } // NewTar will create an instance of Tar that can write files to the writer at f. @@ -52,14 +52,14 @@ func NewTar(f io.Writer) Tar { } } -// NewCanonicalTar will create an instance of Tar that can write files to the +// NewReproducibleTar will create an instance of Tar that can write files to the // writer at f, ignoring timestamps to produce a canonical archive. -func NewCanonicalTar(f io.Writer) Tar { +func NewReproducibleTar(f io.Writer) Tar { w := tar.NewWriter(f) return Tar{ - w: w, - hardlinks: map[uint64]string{}, - canonical: true, + w: w, + hardlinks: map[uint64]string{}, + ignoreTimestamps: true, } } @@ -110,7 +110,7 @@ func (t *Tar) AddFileToTar(p string) error { if err != nil { return err } - if t.canonical { + if t.ignoreTimestamps { ct := time.Time{} hdr.ModTime = ct diff --git a/pkg/util/tar_util_test.go b/pkg/util/tar_util_test.go index eb58da0fae..18ba4e00bc 100644 --- a/pkg/util/tar_util_test.go +++ b/pkg/util/tar_util_test.go @@ -22,6 +22,7 @@ import ( "compress/gzip" "fmt" "io" + "io/fs" "os" "path/filepath" "testing" @@ -153,6 +154,9 @@ func Test_CreateTarballOfDirectory(t *testing.T) { // skip directory continue } + if modTime := fileInfo.ModTime(); modTime.Equal(time.Unix(0, 0)) { + t.Errorf("unexpected modtime %q of %q", modTime, fileInfo.Name()) + } file, err := os.Open(filePath) testutil.CheckError(t, wantErr, err) body, err := io.ReadAll(file) @@ -172,3 +176,56 @@ func createFilesInTempDir(t *testing.T, tmpDir string) { } } } + +func Test_NewReproducibleTar(t *testing.T) { + tmpDir := t.TempDir() + createFilesInTempDir(t, tmpDir) + f := &bytes.Buffer{} + + // Create tarball ignoring timestamps + tw := NewReproducibleTar(f) + if err := filepath.WalkDir(tmpDir, func(path string, _ fs.DirEntry, err error) error { + if err != nil { + return err + } + if !filepath.IsAbs(path) { + return fmt.Errorf("path %v is not absolute", path) + } + return tw.AddFileToTar(path) + }); err != nil { + t.Fatalf("failed to create tar of %q: %s", tmpDir, err.Error()) + } + + extracedFilesDir := filepath.Join(tmpDir, "extracted") + if err := os.Mkdir(extracedFilesDir, 0755); err != nil { + t.Fatal(err) + } + files, err := UnTar(f, extracedFilesDir) + if err != nil { + t.Fatalf("untar: %s", err.Error()) + } + for _, filePath := range files { + fileInfo, err := os.Lstat(filePath) + if err != nil { + t.Fatalf("stat %q: %s", filePath, err.Error()) + } + if fileInfo.IsDir() { + // skip directory + continue + } + // In a 'reproducible' tar, all timestamps should be set to zero + if modTime := fileInfo.ModTime(); !modTime.Equal(time.Unix(0, 0)) { + t.Errorf("unexpected modtime %q of %q", modTime, filePath) + } + file, err := os.Open(filePath) + if err != nil { + t.Fatalf("open file %q: %s", filePath, err.Error()) + } + body, err := io.ReadAll(file) + if err != nil { + t.Fatalf("read file %q: %s", filePath, err.Error()) + } + index := filepath.Base(filePath) + testutil.CheckDeepEqual(t, string(body), fmt.Sprintf("hello from %s\n", index)) + } +}