From 696ae8dd977abcf5b633727c5f8d9ef1bbf101f5 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Tue, 28 May 2024 11:35:07 +0000 Subject: [PATCH 1/6] Add support for fake, and speed up reproducible builds --- pkg/commands/cache.go | 9 +- pkg/commands/copy.go | 23 ++++ pkg/commands/run.go | 23 ++++ pkg/executor/build.go | 232 ++++++++++++++++++++++++++++++++++++++- pkg/snapshot/snapshot.go | 21 +++- pkg/util/tar_util.go | 23 ++++ 6 files changed, 326 insertions(+), 5 deletions(-) diff --git a/pkg/commands/cache.go b/pkg/commands/cache.go index 878440df47..d3a138475d 100644 --- a/pkg/commands/cache.go +++ b/pkg/commands/cache.go @@ -16,7 +16,10 @@ limitations under the License. package commands -import v1 "github.com/google/go-containerregistry/pkg/v1" +import ( + "github.com/GoogleContainerTools/kaniko/pkg/dockerfile" + v1 "github.com/google/go-containerregistry/pkg/v1" +) type Cached interface { Layer() v1.Layer @@ -29,3 +32,7 @@ type caching struct { func (c caching) Layer() v1.Layer { return c.layer } + +type FakeExecuteCommand interface { + FakeExecuteCommand(*v1.Config, *dockerfile.BuildArgs) error +} diff --git a/pkg/commands/copy.go b/pkg/commands/copy.go index 069e7fb5a8..0c72881c9c 100644 --- a/pkg/commands/copy.go +++ b/pkg/commands/copy.go @@ -206,6 +206,29 @@ func (cr *CachingCopyCommand) ExecuteCommand(config *v1.Config, buildArgs *docke return nil } +func (cr *CachingCopyCommand) FakeExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + logrus.Infof("Found cached layer, faking extraction to filesystem") + var err error + + if cr.img == nil { + return errors.New(fmt.Sprintf("cached command image is nil %v", cr.String())) + } + + layers, err := cr.img.Layers() + if err != nil { + return errors.Wrapf(err, "retrieve image layers") + } + + if len(layers) != 1 { + return errors.New(fmt.Sprintf("expected %d layers but got %d", 1, len(layers))) + } + + cr.layer = layers[0] + cr.extractedFiles = []string{} + + return nil +} + func (cr *CachingCopyCommand) FilesUsedFromContext(config *v1.Config, buildArgs *dockerfile.BuildArgs) ([]string, error) { return copyCmdFilesUsedFromContext(config, buildArgs, cr.cmd, cr.fileContext) } diff --git a/pkg/commands/run.go b/pkg/commands/run.go index 3f3e82a906..a1aecbc8b7 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -256,6 +256,29 @@ func (cr *CachingRunCommand) ExecuteCommand(config *v1.Config, buildArgs *docker return nil } +func (cr *CachingRunCommand) FakeExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + logrus.Infof("Found cached layer, faking extraction to filesystem") + var err error + + if cr.img == nil { + return errors.New(fmt.Sprintf("command image is nil %v", cr.String())) + } + + layers, err := cr.img.Layers() + if err != nil { + return errors.Wrap(err, "retrieving image layers") + } + + if len(layers) != 1 { + return errors.New(fmt.Sprintf("expected %d layers but got %d", 1, len(layers))) + } + + cr.layer = layers[0] + cr.extractedFiles = []string{} + + return nil +} + func (cr *CachingRunCommand) FilesToSnapshot() []string { f := cr.extractedFiles logrus.Debugf("%d files extracted by caching run command", len(f)) diff --git a/pkg/executor/build.go b/pkg/executor/build.go index d163af6831..02ce01647b 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -111,7 +111,12 @@ func newStageBuilder(args *dockerfile.BuildArgs, opts *config.KanikoOptions, sta return nil, err } l := snapshot.NewLayeredMap(hasher) - snapshotter := snapshot.NewSnapshotter(l, config.RootDir) + var snapshotter snapShotter + if !opts.Reproducible { + snapshotter = snapshot.NewSnapshotter(l, config.RootDir) + } else { + snapshotter = snapshot.NewCanonicalSnapshotter(l, config.RootDir) + } digest, err := sourceImage.Digest() if err != nil { @@ -444,6 +449,92 @@ func (s *stageBuilder) build() error { return nil } +// fakeBuild is like build(), but does not actually execute the commands or +// extract files. +func (s *stageBuilder) fakeBuild() error { + // Set the initial cache key to be the base image digest, the build args and the SrcContext. + var compositeKey *CompositeCache + if cacheKey, ok := s.digestToCacheKey[s.baseImageDigest]; ok { + compositeKey = NewCompositeCache(cacheKey) + } else { + compositeKey = NewCompositeCache(s.baseImageDigest) + } + + // Apply optimizations to the instructions. + if err := s.optimize(*compositeKey, s.cf.Config); err != nil { + return errors.Wrap(err, "failed to optimize instructions") + } + + for index, command := range s.cmds { + if command == nil { + continue + } + + // If the command uses files from the context, add them. + files, err := command.FilesUsedFromContext(&s.cf.Config, s.args) + if err != nil { + return errors.Wrap(err, "failed to get files used from context") + } + + if s.opts.Cache { + *compositeKey, err = s.populateCompositeKey(command, files, *compositeKey, s.args, s.cf.Config.Env) + if err != nil && s.opts.Cache { + return err + } + } + + logrus.Info(command.String()) + + isCacheCommand := func() bool { + switch command.(type) { + case commands.Cached: + return true + default: + return false + } + }() + + if c, ok := command.(commands.FakeExecuteCommand); ok { + if err := c.FakeExecuteCommand(&s.cf.Config, s.args); err != nil { + return errors.Wrap(err, "failed to execute fake command") + } + } else { + switch command.(type) { + case *commands.UserCommand: + default: + return errors.Errorf("uncached command %T is not supported in fake build", command) + } + if err := command.ExecuteCommand(&s.cf.Config, s.args); err != nil { + return errors.Wrap(err, "failed to execute command") + } + } + files = command.FilesToSnapshot() + + if !s.shouldTakeSnapshot(index, command.MetadataOnly()) && !s.opts.ForceBuildMetadata { + logrus.Debugf("fakeBuild: skipping snapshot for [%v]", command.String()) + continue + } + if isCacheCommand { + v := command.(commands.Cached) + layer := v.Layer() + if err := s.saveLayerToImage(layer, command.String()); err != nil { + return errors.Wrap(err, "failed to save layer") + } + } else { + tarPath, err := s.takeSnapshot(files, command.ShouldDetectDeletedFiles()) + if err != nil { + return errors.Wrap(err, "failed to take snapshot") + } + + if err := s.saveSnapshotToImage(command.String(), tarPath); err != nil { + return errors.Wrap(err, "failed to save snapshot to image") + } + } + } + + return nil +} + func (s *stageBuilder) takeSnapshot(files []string, shdDelete bool) (string, error) { var snapshot string var err error @@ -787,7 +878,9 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) { return nil, err } if opts.Reproducible { - sourceImage, err = mutate.Canonical(sourceImage) + // If this option is enabled, we will use the canonical + // snapshotter to avoid having to modify the layers here. + sourceImage, err = mutateCanonicalWithoutLayerEdit(sourceImage) if err != nil { return nil, err } @@ -797,6 +890,7 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) { return nil, err } } + timing.DefaultRun.Stop(t) return sourceImage, nil } @@ -833,6 +927,140 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) { return nil, err } +// DoFakeBuild executes building the Dockerfile without modifying the +// filesystem, returns an error if build cache is not available. +func DoFakeBuild(opts *config.KanikoOptions) (v1.Image, error) { + digestToCacheKey := make(map[string]string) + stageIdxToDigest := make(map[string]string) + + stages, metaArgs, err := dockerfile.ParseStages(opts) + if err != nil { + return nil, err + } + + kanikoStages, err := dockerfile.MakeKanikoStages(opts, stages, metaArgs) + if err != nil { + return nil, err + } + stageNameToIdx := ResolveCrossStageInstructions(kanikoStages) + + fileContext, err := util.NewFileContextFromDockerfile(opts.DockerfilePath, opts.SrcContext) + if err != nil { + return nil, err + } + + // Some stages may refer to other random images, not previous stages + if err := fetchExtraStages(kanikoStages, opts); err != nil { + return nil, err + } + crossStageDependencies, err := CalculateDependencies(kanikoStages, opts, stageNameToIdx) + if err != nil { + return nil, err + } + logrus.Infof("Built cross stage deps: %v", crossStageDependencies) + + var args *dockerfile.BuildArgs + + for _, stage := range kanikoStages { + sb, err := newStageBuilder( + args, opts, stage, + crossStageDependencies, + digestToCacheKey, + stageIdxToDigest, + stageNameToIdx, + fileContext) + if err != nil { + return nil, err + } + + args = sb.args + if err := sb.fakeBuild(); err != nil { + return nil, errors.Wrap(err, "error fake building stage") + } + + reviewConfig(stage, &sb.cf.Config) + + sourceImage, err := mutate.Config(sb.image, sb.cf.Config) + if err != nil { + return nil, err + } + + configFile, err := sourceImage.ConfigFile() + if err != nil { + return nil, err + } + if opts.CustomPlatform == "" { + configFile.OS = runtime.GOOS + configFile.Architecture = runtime.GOARCH + } else { + configFile.OS = strings.Split(opts.CustomPlatform, "/")[0] + configFile.Architecture = strings.Split(opts.CustomPlatform, "/")[1] + } + sourceImage, err = mutate.ConfigFile(sourceImage, configFile) + if err != nil { + return nil, err + } + + d, err := sourceImage.Digest() + if err != nil { + return nil, err + } + stageIdxToDigest[fmt.Sprintf("%d", sb.stage.Index)] = d.String() + logrus.Infof("Mapping stage idx %v to digest %v", sb.stage.Index, d.String()) + + digestToCacheKey[d.String()] = sb.finalCacheKey + logrus.Infof("Mapping digest %v to cachekey %v", d.String(), sb.finalCacheKey) + + if stage.Final { + sourceImage, err = mutateCanonicalWithoutLayerEdit(sourceImage) + if err != nil { + return nil, err + } + + return sourceImage, nil + } + } + + return nil, err +} + +// From mutate.Canonical with layer de/compress stripped out. +func mutateCanonicalWithoutLayerEdit(image v1.Image) (v1.Image, error) { + t := time.Time{} + + ocf, err := image.ConfigFile() + if err != nil { + return nil, fmt.Errorf("setting config file: %w", err) + } + + cfg := ocf.DeepCopy() + + // Copy basic config over + cfg.Architecture = ocf.Architecture + cfg.OS = ocf.OS + cfg.OSVersion = ocf.OSVersion + cfg.Config = ocf.Config + + // Strip away timestamps from the config file + cfg.Created = v1.Time{Time: t} + + for i, h := range cfg.History { + h.Created = v1.Time{Time: t} + h.CreatedBy = ocf.History[i].CreatedBy + h.Comment = ocf.History[i].Comment + h.EmptyLayer = ocf.History[i].EmptyLayer + // Explicitly ignore Author field; which hinders reproducibility + h.Author = "" + cfg.History[i] = h + } + + cfg.Container = "" + cfg.Config.Hostname = "" + cfg.DockerVersion = "" + + return mutate.ConfigFile(image, cfg) +} + // filesToSave returns all the files matching the given pattern in deps. // If a file is a symlink, it also returns the target file. func filesToSave(deps []string) ([]string, error) { diff --git a/pkg/snapshot/snapshot.go b/pkg/snapshot/snapshot.go index 2a9244693e..450f96ec4d 100644 --- a/pkg/snapshot/snapshot.go +++ b/pkg/snapshot/snapshot.go @@ -42,6 +42,7 @@ type Snapshotter struct { l *LayeredMap directory string ignorelist []util.IgnoreListEntry + canonical bool } // NewSnapshotter creates a new snapshotter rooted at d @@ -49,6 +50,12 @@ 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 +// reproducible snapshots. +func NewCanonicalSnapshotter(l *LayeredMap, d string) *Snapshotter { + return &Snapshotter{l: l, directory: d, ignorelist: util.IgnoreList(), canonical: true} +} + // Init initializes a new snapshotter func (s *Snapshotter) Init() error { logrus.Info("Initializing snapshotter ...") @@ -112,7 +119,12 @@ func (s *Snapshotter) TakeSnapshot(files []string, shdCheckDelete bool, forceBui sort.Strings(filesToWhiteout) } - t := util.NewTar(f) + var t util.Tar + if !s.canonical { + t = util.NewTar(f) + } else { + t = util.NewCanonicalTar(f) + } defer t.Close() if err := writeToTar(t, filesToAdd, filesToWhiteout); err != nil { return "", err @@ -128,7 +140,12 @@ func (s *Snapshotter) TakeSnapshotFS() (string, error) { return "", err } defer f.Close() - t := util.NewTar(f) + var t util.Tar + if !s.canonical { + t = util.NewTar(f) + } else { + t = util.NewCanonicalTar(f) + } defer t.Close() filesToAdd, filesToWhiteOut, err := s.scanFullFilesystem() diff --git a/pkg/util/tar_util.go b/pkg/util/tar_util.go index 467e215149..4dda4d1744 100644 --- a/pkg/util/tar_util.go +++ b/pkg/util/tar_util.go @@ -27,6 +27,7 @@ import ( "path/filepath" "strings" "syscall" + "time" "github.com/GoogleContainerTools/kaniko/pkg/config" "github.com/docker/docker/pkg/archive" @@ -39,6 +40,7 @@ import ( type Tar struct { hardlinks map[uint64]string w *tar.Writer + canonical bool } // NewTar will create an instance of Tar that can write files to the writer at f. @@ -50,6 +52,17 @@ func NewTar(f io.Writer) Tar { } } +// NewCanonicalTar 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 { + w := tar.NewWriter(f) + return Tar{ + w: w, + hardlinks: map[uint64]string{}, + canonical: true, + } +} + func CreateTarballOfDirectory(pathToDir string, f io.Writer) error { if !filepath.IsAbs(pathToDir) { return errors.New("pathToDir is not absolute") @@ -97,6 +110,16 @@ func (t *Tar) AddFileToTar(p string) error { if err != nil { return err } + if t.canonical { + ct := time.Time{} + hdr.ModTime = ct + + // PAX and GNU Format support additional timestamps in the header + if hdr.Format == tar.FormatPAX || hdr.Format == tar.FormatGNU { + hdr.AccessTime = ct + hdr.ChangeTime = ct + } + } err = readSecurityXattrToTarHeader(p, hdr) if err != nil { return err From 0dddc0ab4a65c6f08fa70a283b2d72f086427148 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 7 Jun 2024 14:30:43 +0100 Subject: [PATCH 2/6] rename NewCanonicalSnapshotter -> NewReproducibleSnapshotter --- pkg/executor/build.go | 2 +- pkg/snapshot/snapshot.go | 22 +++++++++++----------- pkg/util/tar_util.go | 18 +++++++++--------- 3 files changed, 21 insertions(+), 21 deletions(-) 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 From c03819e16def1e67b40df85cd345561bb4b7565c Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 7 Jun 2024 14:30:59 +0100 Subject: [PATCH 3/6] add test for NewReproducibleTar --- pkg/util/tar_util_test.go | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) 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)) + } +} From 3a6d6551438efb79569cec8a6edd805df4a63466 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 10 Jun 2024 14:21:34 +0100 Subject: [PATCH 4/6] rename to probeCache --- pkg/executor/build.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pkg/executor/build.go b/pkg/executor/build.go index 6e84ffdf14..5c77520bcb 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -449,9 +449,10 @@ func (s *stageBuilder) build() error { return nil } -// fakeBuild is like build(), but does not actually execute the commands or -// extract files. -func (s *stageBuilder) fakeBuild() error { +// probeCache builds a stage entirely from the build cache. +// All COPY and RUN commands are faked. +// Note: USER and ENV commands are not supported. +func (s *stageBuilder) probeCache() error { // Set the initial cache key to be the base image digest, the build args and the SrcContext. var compositeKey *CompositeCache if cacheKey, ok := s.digestToCacheKey[s.baseImageDigest]; ok { @@ -927,9 +928,10 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) { return nil, err } -// DoFakeBuild executes building the Dockerfile without modifying the -// filesystem, returns an error if build cache is not available. -func DoFakeBuild(opts *config.KanikoOptions) (v1.Image, error) { +// DoCacheProbe builds the Dockerfile relying entirely on the build +// cache without modifying the filesystem. +// Returns an error if any layers are missing from build cache. +func DoCacheProbe(opts *config.KanikoOptions) (v1.Image, error) { digestToCacheKey := make(map[string]string) stageIdxToDigest := make(map[string]string) @@ -974,7 +976,7 @@ func DoFakeBuild(opts *config.KanikoOptions) (v1.Image, error) { } args = sb.args - if err := sb.fakeBuild(); err != nil { + if err := sb.probeCache(); err != nil { return nil, errors.Wrap(err, "error fake building stage") } From 61cdf80e9a2d95f78ea8546af846c8aaeea14ac8 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 10 Jun 2024 14:21:52 +0100 Subject: [PATCH 5/6] add tests for DoCacheProbe --- pkg/executor/cache_probe_test.go | 155 +++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 pkg/executor/cache_probe_test.go diff --git a/pkg/executor/cache_probe_test.go b/pkg/executor/cache_probe_test.go new file mode 100644 index 0000000000..40655c825d --- /dev/null +++ b/pkg/executor/cache_probe_test.go @@ -0,0 +1,155 @@ +package executor + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/GoogleContainerTools/kaniko/pkg/config" + "github.com/GoogleContainerTools/kaniko/pkg/constants" + "github.com/GoogleContainerTools/kaniko/testutil" +) + +func TestDoCacheProbe(t *testing.T) { + t.Run("Empty", func(t *testing.T) { + testDir, fn := setupCacheProbeTests(t) + defer fn() + dockerFile := `FROM scratch +COPY foo/bar.txt copied/ +` + os.WriteFile(filepath.Join(testDir, "workspace", "Dockerfile"), []byte(dockerFile), 0755) + // Populate the cache by doing an initial build + cacheDir := t.TempDir() + opts := &config.KanikoOptions{ + DockerfilePath: filepath.Join(testDir, "workspace", "Dockerfile"), + SrcContext: filepath.Join(testDir, "workspace"), + SnapshotMode: constants.SnapshotModeFull, + Cache: true, + CacheOptions: config.CacheOptions{ + CacheTTL: time.Hour, + }, + CacheCopyLayers: true, + CacheRunLayers: true, + CacheRepo: "oci:/" + cacheDir, + } + _, err := DoCacheProbe(opts) + if err == nil || !strings.Contains(err.Error(), "not supported in fake build") { + t.Errorf("unexpected error, got %v", err) + } + }) + + t.Run("Present", func(t *testing.T) { + testDir, fn := setupCacheProbeTests(t) + defer fn() + dockerFile := `FROM scratch +COPY foo/bar.txt copied/ +` + os.WriteFile(filepath.Join(testDir, "workspace", "Dockerfile"), []byte(dockerFile), 0755) + cacheDir := t.TempDir() + opts := &config.KanikoOptions{ + DockerfilePath: filepath.Join(testDir, "workspace", "Dockerfile"), + SrcContext: filepath.Join(testDir, "workspace"), + SnapshotMode: constants.SnapshotModeRedo, + Cache: true, + CacheOptions: config.CacheOptions{ + CacheTTL: time.Hour, + }, + CacheCopyLayers: true, + CacheRunLayers: true, + CacheRepo: "oci:/" + cacheDir, + } + _, err := DoBuild(opts) + testutil.CheckNoError(t, err) + opts.Reproducible = true + _, err = DoCacheProbe(opts) + testutil.CheckNoError(t, err) + }) + + t.Run("Partial", func(t *testing.T) { + testDir, fn := setupCacheProbeTests(t) + defer fn() + dockerFile := `FROM scratch +COPY foo/bar.txt copied/ +` + os.WriteFile(filepath.Join(testDir, "workspace", "Dockerfile"), []byte(dockerFile), 0755) + cacheDir := t.TempDir() + opts := &config.KanikoOptions{ + DockerfilePath: filepath.Join(testDir, "workspace", "Dockerfile"), + SrcContext: filepath.Join(testDir, "workspace"), + SnapshotMode: constants.SnapshotModeFull, + Cache: true, + CacheOptions: config.CacheOptions{ + CacheTTL: time.Hour, + }, + CacheCopyLayers: true, + CacheRunLayers: true, + CacheRepo: "oci:/" + cacheDir, + } + _, err := DoBuild(opts) + testutil.CheckNoError(t, err) + opts.Reproducible = true + + // Modify the Dockerfile to add some extra steps + dockerFile = `FROM scratch +COPY foo/bar.txt copied/ +COPY foo/baz.txt copied/ +` + os.WriteFile(filepath.Join(testDir, "workspace", "Dockerfile"), []byte(dockerFile), 0755) + _, err = DoCacheProbe(opts) + if err == nil || !strings.Contains(err.Error(), "not supported in fake build") { + t.Errorf("unexpected error, got %v", err) + } + }) +} + +func setupCacheProbeTests(t *testing.T) (string, func()) { + testDir := t.TempDir() + // Create workspace with files, dirs, and symlinks + // workspace tree: + // /root + // /kaniko + // /workspace + // - /foo + // - bar.txt + // - baz.txt + if err := os.MkdirAll(filepath.Join(testDir, "kaniko/0"), 0755); err != nil { + t.Fatal(err) + } + workspace := filepath.Join(testDir, "workspace") + // Make foo + if err := os.MkdirAll(filepath.Join(workspace, "foo"), 0755); err != nil { + t.Fatal(err) + } + file := filepath.Join(workspace, "foo", "bar.txt") + if err := os.WriteFile(file, []byte("hello"), 0755); err != nil { + t.Fatal(err) + } + file2 := filepath.Join(workspace, "foo", "baz.txt") + if err := os.WriteFile(file2, []byte("world"), 0755); err != nil { + t.Fatal(err) + } + + // set up config + config.RootDir = testDir + config.KanikoDir = fmt.Sprintf("%s/%s", testDir, "kaniko") + // Write path to ignore list + if err := os.MkdirAll(filepath.Join(testDir, "proc"), 0755); err != nil { + t.Fatal(err) + } + mFile := filepath.Join(testDir, "proc/mountinfo") + mountInfo := fmt.Sprintf( + `36 35 98:0 /kaniko %s/kaniko rw,noatime master:1 - ext3 /dev/root rw,errors=continue +36 35 98:0 /proc %s/proc rw,noatime master:1 - ext3 /dev/root rw,errors=continue +`, testDir, testDir) + if err := os.WriteFile(mFile, []byte(mountInfo), 0644); err != nil { + t.Fatal(err) + } + config.MountInfoPath = mFile + return testDir, func() { + config.RootDir = constants.RootDir + config.MountInfoPath = constants.MountInfoPath + } +} From 9d76d2f1980252a7f5e56ea1db05a52f59fcc90e Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 10 Jun 2024 14:34:04 +0100 Subject: [PATCH 6/6] blurb --- pkg/executor/cache_probe_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/executor/cache_probe_test.go b/pkg/executor/cache_probe_test.go index 40655c825d..0536c520f0 100644 --- a/pkg/executor/cache_probe_test.go +++ b/pkg/executor/cache_probe_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2018 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package executor import (