diff --git a/pkg/commands/arg.go b/pkg/commands/arg.go index 98bd907f8a..3af8f29392 100644 --- a/pkg/commands/arg.go +++ b/pkg/commands/arg.go @@ -41,6 +41,10 @@ func (r *ArgCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bui return nil } +func (r *ArgCommand) CachedExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + return r.ExecuteCommand(config, buildArgs) +} + func ParseArg(key string, val *string, env []string, ba *dockerfile.BuildArgs) (string, *string, error) { replacementEnvs := ba.ReplacementEnvs(env) resolvedKey, err := util.ResolveEnvironmentReplacement(key, replacementEnvs, false) diff --git a/pkg/commands/cmd.go b/pkg/commands/cmd.go index e3788d9a70..e29f1d52bc 100644 --- a/pkg/commands/cmd.go +++ b/pkg/commands/cmd.go @@ -54,6 +54,10 @@ func (c *CmdCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bui return nil } +func (c *CmdCommand) CachedExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + return c.ExecuteCommand(config, buildArgs) +} + // String returns some information about the command for the image config history func (c *CmdCommand) String() string { return c.cmd.String() diff --git a/pkg/commands/entrypoint.go b/pkg/commands/entrypoint.go index 83964d4faf..7e96eb75e5 100644 --- a/pkg/commands/entrypoint.go +++ b/pkg/commands/entrypoint.go @@ -51,6 +51,10 @@ func (e *EntrypointCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerf return nil } +func (e *EntrypointCommand) CachedExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + return e.ExecuteCommand(config, buildArgs) +} + // String returns some information about the command for the image config history func (e *EntrypointCommand) String() string { return e.cmd.String() diff --git a/pkg/commands/env.go b/pkg/commands/env.go index 2be2714902..387aebfb9f 100644 --- a/pkg/commands/env.go +++ b/pkg/commands/env.go @@ -35,6 +35,10 @@ func (e *EnvCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bui return util.UpdateConfigEnv(newEnvs, config, replacementEnvs) } +func (e *EnvCommand) CachedExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + return e.ExecuteCommand(config, buildArgs) +} + // String returns some information about the command for the image config history func (e *EnvCommand) String() string { return e.cmd.String() diff --git a/pkg/commands/expose.go b/pkg/commands/expose.go index 7346160ac8..5c4e2be489 100644 --- a/pkg/commands/expose.go +++ b/pkg/commands/expose.go @@ -63,6 +63,10 @@ func (r *ExposeCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile. return nil } +func (r *ExposeCommand) CachedExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + return r.ExecuteCommand(config, buildArgs) +} + func validProtocol(protocol string) bool { validProtocols := [2]string{"tcp", "udp"} for _, p := range validProtocols { diff --git a/pkg/commands/healthcheck.go b/pkg/commands/healthcheck.go index 55474bb781..935bcffeac 100644 --- a/pkg/commands/healthcheck.go +++ b/pkg/commands/healthcheck.go @@ -46,6 +46,10 @@ func (h *HealthCheckCommand) ExecuteCommand(config *v1.Config, buildArgs *docker return nil } +func (h *HealthCheckCommand) CachedExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + return h.ExecuteCommand(config, buildArgs) +} + // String returns some information about the command for the image config history func (h *HealthCheckCommand) String() string { return h.cmd.String() diff --git a/pkg/commands/label.go b/pkg/commands/label.go index 7568886c0d..04f5b8d702 100644 --- a/pkg/commands/label.go +++ b/pkg/commands/label.go @@ -34,6 +34,10 @@ func (r *LabelCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.B return updateLabels(r.cmd.Labels, config, buildArgs) } +func (r *LabelCommand) CachedExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + return r.ExecuteCommand(config, buildArgs) +} + func updateLabels(labels []instructions.KeyValuePair, config *v1.Config, buildArgs *dockerfile.BuildArgs) error { existingLabels := config.Labels if existingLabels == nil { diff --git a/pkg/commands/onbuild.go b/pkg/commands/onbuild.go index adfb5a9178..3c64ef5475 100644 --- a/pkg/commands/onbuild.go +++ b/pkg/commands/onbuild.go @@ -40,6 +40,10 @@ func (o *OnBuildCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile return nil } +func (o *OnBuildCommand) CachedExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + return o.ExecuteCommand(config, buildArgs) +} + // String returns some information about the command for the image config history func (o *OnBuildCommand) String() string { return o.cmd.String() diff --git a/pkg/commands/run_marker.go b/pkg/commands/run_marker.go index 352ed94544..53ff6e3aa7 100644 --- a/pkg/commands/run_marker.go +++ b/pkg/commands/run_marker.go @@ -47,6 +47,12 @@ func (r *RunMarkerCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfi return nil } +func (r *RunMarkerCommand) CachedExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + // TODO(mafredri): Check if we need to do more here. + r.Files = []string{} + return nil +} + // String returns some information about the command for the image config func (r *RunMarkerCommand) String() string { return r.cmd.String() diff --git a/pkg/commands/shell.go b/pkg/commands/shell.go index 996c928fd1..af44c19aaf 100644 --- a/pkg/commands/shell.go +++ b/pkg/commands/shell.go @@ -33,6 +33,10 @@ func (s *ShellCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.B return nil } +func (s *ShellCommand) CachedExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + return s.ExecuteCommand(config, buildArgs) +} + // String returns some information about the command for the image config history func (s *ShellCommand) String() string { return s.cmd.String() diff --git a/pkg/commands/stopsignal.go b/pkg/commands/stopsignal.go index 964b6b703c..3316fb347b 100644 --- a/pkg/commands/stopsignal.go +++ b/pkg/commands/stopsignal.go @@ -53,6 +53,10 @@ func (s *StopSignalCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerf return nil } +func (s *StopSignalCommand) CachedExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + return s.ExecuteCommand(config, buildArgs) +} + // String returns some information about the command for the image config history func (s *StopSignalCommand) String() string { return s.cmd.String() diff --git a/pkg/commands/user.go b/pkg/commands/user.go index 937a16a691..43093d1592 100644 --- a/pkg/commands/user.go +++ b/pkg/commands/user.go @@ -55,6 +55,10 @@ func (r *UserCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bu return nil } +func (r *UserCommand) CachedExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + return r.ExecuteCommand(config, buildArgs) +} + func (r *UserCommand) String() string { return r.cmd.String() } diff --git a/pkg/commands/volume.go b/pkg/commands/volume.go index 4abd26cbe0..d4ae3dd17c 100644 --- a/pkg/commands/volume.go +++ b/pkg/commands/volume.go @@ -63,6 +63,28 @@ func (v *VolumeCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile. return nil } +func (v *VolumeCommand) CachedExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + logrus.Info("Cmd: VOLUME") + volumes := v.cmd.Volumes + replacementEnvs := buildArgs.ReplacementEnvs(config.Env) + resolvedVolumes, err := util.ResolveEnvironmentReplacementList(volumes, replacementEnvs, true) + if err != nil { + return err + } + existingVolumes := config.Volumes + if existingVolumes == nil { + existingVolumes = map[string]struct{}{} + } + for _, volume := range resolvedVolumes { + var x struct{} + existingVolumes[volume] = x + util.AddVolumePathToIgnoreList(volume) + } + config.Volumes = existingVolumes + + return nil +} + func (v *VolumeCommand) FilesToSnapshot() []string { return []string{} } diff --git a/pkg/commands/workdir.go b/pkg/commands/workdir.go index eefa978b84..b97c4da841 100644 --- a/pkg/commands/workdir.go +++ b/pkg/commands/workdir.go @@ -79,6 +79,30 @@ func (w *WorkdirCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile return nil } +func (w *WorkdirCommand) CachedExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + logrus.Info("Cmd: workdir") + workdirPath := w.cmd.Path + replacementEnvs := buildArgs.ReplacementEnvs(config.Env) + resolvedWorkingDir, err := util.ResolveEnvironmentReplacement(workdirPath, replacementEnvs, true) + if err != nil { + return err + } + if filepath.IsAbs(resolvedWorkingDir) { + config.WorkingDir = resolvedWorkingDir + } else { + if config.WorkingDir != "" { + config.WorkingDir = filepath.Join(config.WorkingDir, resolvedWorkingDir) + } else { + config.WorkingDir = filepath.Join("/", resolvedWorkingDir) + } + } + logrus.Infof("Changed working directory to %s", config.WorkingDir) + + // TODO(mafredri): Check if we need to do more here. + w.snapshotFiles = []string{} + return nil +} + // FilesToSnapshot returns the workingdir, which should have been created if it didn't already exist func (w *WorkdirCommand) FilesToSnapshot() []string { return w.snapshotFiles diff --git a/pkg/executor/build.go b/pkg/executor/build.go index a34b1684c6..0ba12a7131 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -451,7 +451,6 @@ func (s *stageBuilder) build() 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 @@ -501,12 +500,17 @@ func (s *stageBuilder) probeCache() error { } } else { switch command.(type) { - case *commands.UserCommand: + case *commands.AddCommand: + // The ADD directive does not support caching. + return errors.Errorf("ADD is not supported in cache probe mode, use COPY instead") + case *commands.CopyCommand: + // If the cache is valid, we expect CachingCopyCommand. + return errors.Errorf("uncached COPY command is not supported in cache probe mode") + case *commands.RunCommand: + // If the cache is valid, we expect CachingRunCommand. + return errors.Errorf("uncached RUN command is not supported in cache probe mode") default: - return errors.Errorf("uncached command %T encountered when probing cache", command) - } - if err := command.ExecuteCommand(&s.cf.Config, s.args); err != nil { - return errors.Wrap(err, "failed to execute command") + return errors.Errorf("unsupported command %T encountered in cache probe mode, missing CachedExecuteCommand", command) } } files = command.FilesToSnapshot() diff --git a/pkg/executor/cache_probe_test.go b/pkg/executor/cache_probe_test.go index 2491532c5d..53e04db260 100644 --- a/pkg/executor/cache_probe_test.go +++ b/pkg/executor/cache_probe_test.go @@ -40,7 +40,8 @@ func TestDoCacheProbe(t *testing.T) { dockerFile := `FROM scratch COPY foo/bar.txt copied/ ` - os.WriteFile(filepath.Join(testDir, "workspace", "Dockerfile"), []byte(dockerFile), 0755) + err := os.WriteFile(filepath.Join(testDir, "workspace", "Dockerfile"), []byte(dockerFile), 0o755) + testutil.CheckNoError(t, err) regCache := setupCacheRegistry(t) opts := &config.KanikoOptions{ DockerfilePath: filepath.Join(testDir, "workspace", "Dockerfile"), @@ -54,8 +55,8 @@ COPY foo/bar.txt copied/ CacheRunLayers: true, CacheRepo: regCache + "/test", } - _, err := DoCacheProbe(opts) - if err == nil || !strings.Contains(err.Error(), "uncached command") { + _, err = DoCacheProbe(opts) + if err == nil || !strings.Contains(err.Error(), "uncached COPY command") { t.Errorf("unexpected error, got %v", err) } }) @@ -66,7 +67,8 @@ COPY foo/bar.txt copied/ dockerFile := `FROM scratch COPY foo/bar.txt copied/ ` - os.WriteFile(filepath.Join(testDir, "workspace", "Dockerfile"), []byte(dockerFile), 0755) + err := os.WriteFile(filepath.Join(testDir, "workspace", "Dockerfile"), []byte(dockerFile), 0o755) + testutil.CheckNoError(t, err) regCache := setupCacheRegistry(t) opts := &config.KanikoOptions{ DockerfilePath: filepath.Join(testDir, "workspace", "Dockerfile"), @@ -79,22 +81,26 @@ COPY foo/bar.txt copied/ CacheCopyLayers: true, CacheRunLayers: true, CacheRepo: regCache + "/test", + Reproducible: true, } // Populate the cache by doing an initial build - _, err := DoBuild(opts) - testutil.CheckNoError(t, err) - opts.Reproducible = true + _, err = DoBuild(opts) + if err != nil { + t.Fatalf("build failed: %+v", err) + } _, err = DoCacheProbe(opts) testutil.CheckNoError(t, err) }) - t.Run("Partial", func(t *testing.T) { + t.Run("Envs and args", func(t *testing.T) { testDir, fn := setupCacheProbeTests(t) defer fn() dockerFile := `FROM scratch -COPY foo/bar.txt copied/ +ARG foo=bar +ENV baz=qux ` - os.WriteFile(filepath.Join(testDir, "workspace", "Dockerfile"), []byte(dockerFile), 0755) + err := os.WriteFile(filepath.Join(testDir, "workspace", "Dockerfile"), []byte(dockerFile), 0o755) + testutil.CheckNoError(t, err) regCache := setupCacheRegistry(t) opts := &config.KanikoOptions{ DockerfilePath: filepath.Join(testDir, "workspace", "Dockerfile"), @@ -107,35 +113,25 @@ COPY foo/bar.txt copied/ CacheCopyLayers: true, CacheRunLayers: true, CacheRepo: regCache + "/test", + Reproducible: true, } - _, 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(), "uncached command") { - t.Errorf("unexpected error, got %v", err) + // Populate the cache by doing an initial build + _, err = DoBuild(opts) + if err != nil { + t.Fatalf("build failed: %+v", err) } + _, err = DoCacheProbe(opts) + testutil.CheckNoError(t, err) }) - t.Run("MultiStage", func(t *testing.T) { - t.Skip("TODO: https://github.com/coder/envbuilder/issues/230") - testDir, fn := setupMultistageTests(t) + t.Run("Partial", func(t *testing.T) { + testDir, fn := setupCacheProbeTests(t) defer fn() - dockerFile := ` - FROM scratch as first - COPY foo/bam.txt copied/ - ENV test test - - From scratch as second - COPY --from=first copied/bam.txt output/bam.txt` - os.WriteFile(filepath.Join(testDir, "workspace", "Dockerfile"), []byte(dockerFile), 0755) + dockerFile := `FROM scratch +COPY foo/bar.txt copied/ +` + err := os.WriteFile(filepath.Join(testDir, "workspace", "Dockerfile"), []byte(dockerFile), 0o755) + testutil.CheckNoError(t, err) regCache := setupCacheRegistry(t) opts := &config.KanikoOptions{ DockerfilePath: filepath.Join(testDir, "workspace", "Dockerfile"), @@ -148,20 +144,88 @@ COPY foo/baz.txt copied/ CacheCopyLayers: true, CacheRunLayers: true, CacheRepo: regCache + "/test", + Reproducible: true, } - _, err := DoBuild(opts) + _, err = DoBuild(opts) + if err != nil { + t.Fatalf("build failed: %+v", err) + } + + // Modify the Dockerfile to add some extra steps + dockerFile = `FROM scratch +COPY foo/bar.txt copied/ +COPY foo/baz.txt copied/ +` + err = os.WriteFile(filepath.Join(testDir, "workspace", "Dockerfile"), []byte(dockerFile), 0o755) testutil.CheckNoError(t, err) - os.WriteFile(filepath.Join(testDir, "workspace", "Dockerfile"), []byte(dockerFile), 0755) - opts.Reproducible = true _, err = DoCacheProbe(opts) + if err == nil || !strings.Contains(err.Error(), "uncached COPY command") { + t.Errorf("unexpected error, got %v", err) + } + }) + + t.Run("MultiStage", func(t *testing.T) { + t.Skip("TODO: https://github.com/coder/envbuilder/issues/230") + + // Share cache between both builds. + regCache := setupCacheRegistry(t) + + prepare := func() (*config.KanikoOptions, func()) { + testDir, fn := setupMultistageTests(t) + dockerFile := ` + FROM scratch as first + COPY foo/bam.txt copied/ + ENV test test + + From scratch as second + COPY --from=first copied/bam.txt output/bam.txt` + err := os.WriteFile(filepath.Join(testDir, "workspace", "Dockerfile"), []byte(dockerFile), 0o755) + testutil.CheckNoError(t, err) + 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: regCache + "/test", + Reproducible: true, + // ForceUnpack: true, + Destinations: []string{regCache + "/test"}, + } + return opts, fn + } + + opts, fn := prepare() + defer fn() + image1, err := DoBuild(opts) + if err != nil { + t.Fatalf("build failed: %+v", err) + } + digest1, err := image1.Digest() testutil.CheckNoError(t, err) - // Check Image has one layer bam.txt - files, err := readDirectory(filepath.Join(testDir, "output")) + + err = DoPush(image1, opts) if err != nil { - t.Fatal(err) + t.Fatalf("push failed: %+v", err) + } + + fn() // Clean up build. + + // Start cache probe from a clean slate. + opts, fn = prepare() + defer fn() + image2, err := DoCacheProbe(opts) + testutil.CheckNoError(t, err) + digest2, err := image2.Digest() + testutil.CheckNoError(t, err) + + if digest1.String() != digest2.String() { + t.Errorf("expected %s, got %s", digest1.String(), digest2.String()) } - testutil.CheckDeepEqual(t, 1, len(files)) - testutil.CheckDeepEqual(t, files[0].Name(), "bam.txt") }) }