Repository: coder/envbuilder
Pull request #293 (2024-08-02 08:37:22)

fix: always add COPY directives in DoCacheProbe
Required in order to correctly handle RunCacheProbe. Without it, we will be unable to correctly detect the final layer of the built image.


Patch:
diff --git a/envbuilder.go b/envbuilder.go
index c65ae62..6bebd2e 100644
--- a/envbuilder.go
+++ b/envbuilder.go
@@ -1061,6 +1061,26 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error)
 		})
 	}
 
+	// We expect an image built and pushed by envbuilder to have the envbuilder
+	// binary present at a predefined path. In order to correctly replicate the
+	// build via executor.RunCacheProbe we need to have the *exact* copy of the
+	// envbuilder binary available used to build the image.
+	exePath := opts.BinaryPath
+	// Add an exception for the current running binary in kaniko ignore list
+	if err := util.AddAllowedPathToDefaultIgnoreList(exePath); err != nil {
+		return nil, xerrors.Errorf("add exe path to ignore list: %w", err)
+	}
+	// Copy the envbuilder binary into the build context.
+	buildParams.DockerfileContent += fmt.Sprintf(`
+COPY --chmod=0755 %s %s
+USER root
+WORKDIR /
+ENTRYPOINT [%q]`, exePath, exePath, exePath)
+	dst := filepath.Join(buildParams.BuildContext, exePath)
+	if err := copyFile(exePath, dst); err != nil {
+		return nil, xerrors.Errorf("copy envbuilder binary to build context: %w", err)
+	}
+
 	stdoutWriter, closeStdout := log.Writer(opts.Logger)
 	defer closeStdout()
 	stderrWriter, closeStderr := log.Writer(opts.Logger)
diff --git a/options/defaults.go b/options/defaults.go
index 18bf12a..42e4806 100644
--- a/options/defaults.go
+++ b/options/defaults.go
@@ -55,4 +55,7 @@ func (o *Options) SetDefaults() {
 	if o.WorkspaceFolder == "" {
 		o.WorkspaceFolder = DefaultWorkspaceFolder(o.GitURL)
 	}
+	if o.BinaryPath == "" {
+		o.BinaryPath = "/.envbuilder/bin/envbuilder"
+	}
 }
diff --git a/options/options.go b/options/options.go
index 3432cee..ff6a8be 100644
--- a/options/options.go
+++ b/options/options.go
@@ -156,6 +156,11 @@ type Options struct {
 	// used to improving cache utilization when multiple users are building
 	// working on the same repository.
 	RemoteRepoBuildMode bool
+
+	// BinaryPath is the path to the local envbuilder binary when
+	// attempting to probe the build cache. This is only relevant when
+	// GetCachedImage is true.
+	BinaryPath string
 }
 
 const envPrefix = "ENVBUILDER_"
@@ -427,6 +432,13 @@ func (o *Options) CLI() serpent.OptionSet {
 			Description: "Print the digest of the cached image, if available. " +
 				"Exits with an error if not found.",
 		},
+		{
+			Flag:        "binary-path",
+			Env:         WithEnvPrefix("BINARY_PATH"),
+			Value:       serpent.StringOf(&o.BinaryPath),
+			Hidden:      true,
+			Description: "Specify the path to an Envbuilder binary for use when probing the build cache.",
+		},
 		{
 			Flag:    "remote-repo-build-mode",
 			Env:     WithEnvPrefix("REMOTE_REPO_BUILD_MODE"),
@@ -485,6 +497,9 @@ func (o *Options) Markdown() string {
 	_, _ = sb.WriteString("| - | - | - | - |\n")
 
 	for _, opt := range cliOptions {
+		if opt.Hidden {
+			continue
+		}
 		d := opt.Default
 		if d != "" {
 			d = "`" + d + "`"


Test patch:
diff --git a/integration/integration_test.go b/integration/integration_test.go
index b3fe7be..297e86a 100644
--- a/integration/integration_test.go
+++ b/integration/integration_test.go
@@ -1114,13 +1114,14 @@ RUN date --utc > /root/date.txt`, testImageAlpine),
 		_, err = remote.Image(ref)
 		require.ErrorContains(t, err, "MANIFEST_UNKNOWN", "expected image to not be present before build + push")
 
-		// Then: re-running envbuilder with GET_CACHED_IMAGE should succeed
+		// Then: re-running envbuilder with GET_CACHED_IMAGE should not succeed, as
+		// the envbuilder binary is not present in the pushed image.
 		_, err = runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("CACHE_REPO", testRepo),
 			envbuilderEnv("GET_CACHED_IMAGE", "1"),
 		}})
-		require.NoError(t, err)
+		require.ErrorContains(t, err, "uncached COPY command is not supported in cache probe mode")
 	})
 
 	t.Run("CacheAndPush", func(t *testing.T) {
diff --git a/options/defaults_test.go b/options/defaults_test.go
index 156ae16..4878358 100644
--- a/options/defaults_test.go
+++ b/options/defaults_test.go
@@ -85,6 +85,7 @@ func TestOptions_SetDefaults(t *testing.T) {
 		Filesystem:      chmodfs.New(osfs.New("/")),
 		GitURL:          "",
 		WorkspaceFolder: constants.EmptyWorkspaceDir,
+		BinaryPath:      "/.envbuilder/bin/envbuilder",
 	}
 
 	var actual options.Options