From 4077a87dca6852f7cb6dbcb4fa7005b9f3f7f5fd Mon Sep 17 00:00:00 2001
From: Cian Johnston <cian@coder.com>
Date: Tue, 10 Sep 2024 09:47:39 +0100
Subject: [PATCH] fix: set MagicDir to tempdir when performing cache probe
 (#48)

* fix: set MagicDir to tempdir when performing cache probe

* chore: update envbuilder to b7781d8

* imgutil: get default envbuilder binary path from envbuilder options
---
 go.mod                                        |  4 +-
 go.sum                                        |  8 ++--
 internal/imgutil/imgutil.go                   |  7 +++-
 internal/provider/cached_image_resource.go    | 12 ++----
 .../provider/cached_image_resource_test.go    |  9 +++--
 internal/provider/provider_internal_test.go   |  2 -
 internal/provider/provider_test.go            | 38 +++++++++++++------
 testutil/registrytest/registrytest.go         | 22 ++++++++++-
 8 files changed, 67 insertions(+), 35 deletions(-)

diff --git a/go.mod b/go.mod
index d05a28a..6d869ee 100644
--- a/go.mod
+++ b/go.mod
@@ -10,7 +10,7 @@ replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20240702054557-aa55
 
 require (
 	github.com/GoogleContainerTools/kaniko v1.9.2
-	github.com/coder/envbuilder v1.0.0-rc.0.0.20240830145058-fb7e689f39ed
+	github.com/coder/envbuilder v1.0.0-rc.0.0.20240910082823-b7781d802f88
 	github.com/coder/serpent v0.7.0
 	github.com/docker/docker v26.1.5+incompatible
 	github.com/gliderlabs/ssh v0.3.7
@@ -295,7 +295,7 @@ require (
 	go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516 // indirect
 	golang.org/x/crypto v0.26.0 // indirect
 	golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
-	golang.org/x/mod v0.19.0 // indirect
+	golang.org/x/mod v0.21.0 // indirect
 	golang.org/x/net v0.26.0 // indirect
 	golang.org/x/oauth2 v0.20.0 // indirect
 	golang.org/x/sync v0.8.0 // indirect
diff --git a/go.sum b/go.sum
index 3678f90..744e4f0 100644
--- a/go.sum
+++ b/go.sum
@@ -186,8 +186,8 @@ github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoC
 github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
 github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352 h1:L/EjCuZxs5tOcqqCaASj/nu65TRYEFcTt8qRQfHZXX0=
 github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352/go.mod h1:P1KoQSgnKEAG6Mnd3YlGzAophty+yKA9VV48LpfNRvo=
-github.com/coder/envbuilder v1.0.0-rc.0.0.20240830145058-fb7e689f39ed h1:sDEjs9qB2uJ7O85vGmzMja99IZuLvesxElOUFyy22UY=
-github.com/coder/envbuilder v1.0.0-rc.0.0.20240830145058-fb7e689f39ed/go.mod h1:LWImvtIWaX3eiAI3zyU46WE/PrE099nCut1zJultSk0=
+github.com/coder/envbuilder v1.0.0-rc.0.0.20240910082823-b7781d802f88 h1:eXOILD2tWepnV1r7XZalBX0yC4NJMnpf6OP1nF8O2Ak=
+github.com/coder/envbuilder v1.0.0-rc.0.0.20240910082823-b7781d802f88/go.mod h1:krXpDmUsORgNNdvBe6tnwWCGGDLhabom1UUqAZq9+v0=
 github.com/coder/kaniko v0.0.0-20240830141327-f307586e3dca h1:PrcSWrllqipTrtet50a3VyAJEQmjziIZyhpy0bsC6o0=
 github.com/coder/kaniko v0.0.0-20240830141327-f307586e3dca/go.mod h1:XoTDIhNF0Ll4tLmRYdOn31udU9w5zFrY2PME/crSRCA=
 github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
@@ -895,8 +895,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
-golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
+golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
diff --git a/internal/imgutil/imgutil.go b/internal/imgutil/imgutil.go
index 5c2d96a..5c04441 100644
--- a/internal/imgutil/imgutil.go
+++ b/internal/imgutil/imgutil.go
@@ -7,8 +7,9 @@ import (
 	"io"
 	"os"
 	"path/filepath"
+	"strings"
 
-	"github.com/coder/envbuilder/constants"
+	eboptions "github.com/coder/envbuilder/options"
 	"github.com/google/go-containerregistry/pkg/authn"
 	"github.com/google/go-containerregistry/pkg/name"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -34,7 +35,9 @@ func GetRemoteImage(imgRef string) (v1.Image, error) {
 // ExtractEnvbuilderFromImage reads the image located at imgRef and extracts
 // MagicBinaryLocation to destPath.
 func ExtractEnvbuilderFromImage(ctx context.Context, imgRef, destPath string) error {
-	needle := filepath.Clean(constants.MagicBinaryLocation)[1:] // skip leading '/'
+	var o eboptions.Options
+	o.SetDefaults()
+	needle := strings.TrimPrefix(o.BinaryPath, "/")
 	img, err := GetRemoteImage(imgRef)
 	if err != nil {
 		return fmt.Errorf("check remote image: %w", err)
diff --git a/internal/provider/cached_image_resource.go b/internal/provider/cached_image_resource.go
index 27adc98..9378094 100644
--- a/internal/provider/cached_image_resource.go
+++ b/internal/provider/cached_image_resource.go
@@ -10,7 +10,6 @@ import (
 
 	kconfig "github.com/GoogleContainerTools/kaniko/pkg/config"
 	"github.com/coder/envbuilder"
-	"github.com/coder/envbuilder/constants"
 	eboptions "github.com/coder/envbuilder/options"
 	"github.com/coder/terraform-provider-envbuilder/internal/imgutil"
 	"github.com/coder/terraform-provider-envbuilder/internal/tfutil"
@@ -455,7 +454,7 @@ func runCacheProbe(ctx context.Context, builderImage string, opts eboptions.Opti
 	}()
 
 	oldKanikoDir := kconfig.KanikoDir
-	tmpKanikoDir := filepath.Join(tmpDir, constants.MagicDir)
+	tmpKanikoDir := filepath.Join(tmpDir, ".envbuilder")
 	// Normally you would set the KANIKO_DIR environment variable, but we are importing kaniko directly.
 	kconfig.KanikoDir = tmpKanikoDir
 	tflog.Info(ctx, "set kaniko dir to "+tmpKanikoDir)
@@ -467,6 +466,8 @@ func runCacheProbe(ctx context.Context, builderImage string, opts eboptions.Opti
 	if err := os.MkdirAll(tmpKanikoDir, 0o755); err != nil {
 		return nil, fmt.Errorf("failed to create kaniko dir: %w", err)
 	}
+	// Use the temporary directory as our 'magic dir'.
+	opts.MagicDirBase = tmpKanikoDir
 
 	// In order to correctly reproduce the final layer of the cached image, we
 	// need the envbuilder binary used to originally build the image!
@@ -496,13 +497,6 @@ func runCacheProbe(ctx context.Context, builderImage string, opts eboptions.Opti
 		tflog.Debug(ctx, "workspace_folder not specified, using temp dir", map[string]any{"workspace_folder": opts.WorkspaceFolder})
 	}
 
-	// We need a place to clone the repo.
-	repoDir := filepath.Join(tmpDir, "repo")
-	if err := os.MkdirAll(repoDir, 0o755); err != nil {
-		return nil, fmt.Errorf("failed to create repo dir: %w", err)
-	}
-	opts.RemoteRepoDir = repoDir
-
 	// The below options are not relevant and are set to their zero value
 	// explicitly.
 	// They must be set by extra_env to be used in the final builder image.
diff --git a/internal/provider/cached_image_resource_test.go b/internal/provider/cached_image_resource_test.go
index 00b50ff..6b6c832 100644
--- a/internal/provider/cached_image_resource_test.go
+++ b/internal/provider/cached_image_resource_test.go
@@ -46,6 +46,7 @@ func TestAccCachedImageResource(t *testing.T) {
 						"CODER_AGENT_TOKEN", "some-token",
 						"CODER_AGENT_URL", "https://coder.example.com",
 						"ENVBUILDER_CACHE_REPO", deps.CacheRepo,
+						"ENVBUILDER_DOCKER_CONFIG_BASE64", deps.DockerConfigBase64,
 						"ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH", deps.Repo.Key,
 						"ENVBUILDER_GIT_URL", deps.Repo.URL,
 						"ENVBUILDER_REMOTE_REPO_BUILD_MODE", "true",
@@ -78,6 +79,7 @@ RUN date > /date.txt`,
 						"CODER_AGENT_TOKEN", "some-token",
 						"CODER_AGENT_URL", "https://coder.example.com",
 						"ENVBUILDER_CACHE_REPO", deps.CacheRepo,
+						"ENVBUILDER_DOCKER_CONFIG_BASE64", deps.DockerConfigBase64,
 						"ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH", deps.Repo.Key,
 						"ENVBUILDER_GIT_URL", deps.Repo.URL,
 						"ENVBUILDER_REMOTE_REPO_BUILD_MODE", "true",
@@ -88,9 +90,8 @@ RUN date > /date.txt`,
 			},
 		},
 		{
-			// This test case ensures that parameters passed via extra_env are
-			// handled correctly.
-			name: "extra_env",
+			// This test case ensures that overriding the devcontainer directory works.
+			name: "different_dir",
 			files: map[string]string{
 				"path/to/.devcontainer/devcontainer.json": `{"build": { "dockerfile": "Dockerfile" }}`,
 				"path/to/.devcontainer/Dockerfile": `FROM localhost:5000/test-ubuntu:latest
@@ -115,6 +116,7 @@ RUN date > /date.txt`,
 						"ENVBUILDER_DEVCONTAINER_DIR", "path/to/.devcontainer",
 						"ENVBUILDER_DEVCONTAINER_JSON_PATH", "path/to/.devcontainer/devcontainer.json",
 						"ENVBUILDER_DOCKERFILE_PATH", "path/to/.devcontainer/Dockerfile",
+						"ENVBUILDER_DOCKER_CONFIG_BASE64", deps.DockerConfigBase64,
 						"ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH", deps.Repo.Key,
 						"ENVBUILDER_GIT_URL", deps.Repo.URL,
 						"ENVBUILDER_REMOTE_REPO_BUILD_MODE", "true",
@@ -149,6 +151,7 @@ RUN date > /date.txt`,
 						"CODER_AGENT_URL", "https://coder.example.com",
 						"ENVBUILDER_CACHE_REPO", deps.CacheRepo,
 						"ENVBUILDER_DOCKERFILE_PATH", "Dockerfile",
+						"ENVBUILDER_DOCKER_CONFIG_BASE64", deps.DockerConfigBase64,
 						"ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH", deps.Repo.Key,
 						"ENVBUILDER_GIT_URL", deps.Repo.URL,
 						"ENVBUILDER_REMOTE_REPO_BUILD_MODE", "true",
diff --git a/internal/provider/provider_internal_test.go b/internal/provider/provider_internal_test.go
index a9be0ae..aedfca2 100644
--- a/internal/provider/provider_internal_test.go
+++ b/internal/provider/provider_internal_test.go
@@ -272,7 +272,6 @@ func Test_computeEnvFromOptions(t *testing.T) {
 				PostStartScriptPath:  "string",
 				PushImage:            true,
 				RemoteRepoBuildMode:  true,
-				RemoteRepoDir:        "string",
 				SetupScript:          "string",
 				SkipRebuild:          true,
 				SSLCertBase64:        "string",
@@ -314,7 +313,6 @@ func Test_computeEnvFromOptions(t *testing.T) {
 				"ENVBUILDER_POST_START_SCRIPT_PATH":   "string",
 				"ENVBUILDER_PUSH_IMAGE":               "true",
 				"ENVBUILDER_REMOTE_REPO_BUILD_MODE":   "true",
-				"ENVBUILDER_REMOTE_REPO_DIR":          "string",
 				"ENVBUILDER_SETUP_SCRIPT":             "string",
 				"ENVBUILDER_SKIP_REBUILD":             "true",
 				"ENVBUILDER_SSL_CERT_BASE64":          "string",
diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go
index 26dc7d4..ef58b73 100644
--- a/internal/provider/provider_test.go
+++ b/internal/provider/provider_test.go
@@ -3,6 +3,7 @@ package provider
 import (
 	"bufio"
 	"context"
+	"encoding/base64"
 	"fmt"
 	"io"
 	"os"
@@ -35,10 +36,11 @@ var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServe
 
 // testDependencies contain information about stuff the test depends on.
 type testDependencies struct {
-	BuilderImage string
-	CacheRepo    string
-	ExtraEnv     map[string]string
-	Repo         testGitRepoSSH
+	BuilderImage       string
+	CacheRepo          string
+	DockerConfigBase64 string
+	ExtraEnv           map[string]string
+	Repo               testGitRepoSSH
 }
 
 // Config generates a valid Terraform config file from the dependencies.
@@ -47,8 +49,9 @@ func (d *testDependencies) Config(t testing.TB) string {
 
 	tpl := `provider envbuilder {}
 resource "envbuilder_cached_image" "test" {
-  builder_image            = {{ quote .BuilderImage }}
+  builder_image              = {{ quote .BuilderImage }}
 	cache_repo               = {{ quote .CacheRepo }}
+	docker_config_base64     = {{ quote .DockerConfigBase64 }}
 	git_url                  = {{ quote .Repo.URL }}
 	extra_env                = {
 		"ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH": {{ quote .Repo.Key }}
@@ -78,19 +81,29 @@ func setup(ctx context.Context, t testing.TB, extraEnv, files map[string]string)
 	envbuilderVersion := getEnvOrDefault("ENVBUILDER_VERSION", "latest")
 	envbuilderImageRef := envbuilderImage + ":" + envbuilderVersion
 
-	// TODO: envbuilder creates /.envbuilder/bin/envbuilder owned by root:root which we are unable to clean up.
-	// This causes tests to fail.
+	testUsername := "testuser"
+	testPassword := "testpassword"
+	testAuthBase64 := base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", testUsername, testPassword)))
 	regDir := t.TempDir()
-	reg := registrytest.New(t, regDir)
+	reg := registrytest.New(t, regDir, registrytest.BasicAuthMW(t, testUsername, testPassword))
 
 	repoDir := setupGitRepo(t, files)
 	gitRepo := serveGitRepoSSH(ctx, t, repoDir)
+	dockerConfigJSON := fmt.Sprintf(`{
+		"auths": {
+			"%s": {
+				"auth": "%s",
+			}
+		}
+	}`, reg, testAuthBase64)
+	dockerConfigJSONBase64 := base64.StdEncoding.EncodeToString([]byte(dockerConfigJSON))
 
 	return testDependencies{
-		BuilderImage: envbuilderImageRef,
-		CacheRepo:    reg + "/test",
-		ExtraEnv:     extraEnv,
-		Repo:         gitRepo,
+		BuilderImage:       envbuilderImageRef,
+		CacheRepo:          reg + "/test",
+		ExtraEnv:           extraEnv,
+		Repo:               gitRepo,
+		DockerConfigBase64: dockerConfigJSONBase64,
 	}
 }
 
@@ -115,6 +128,7 @@ func seedCache(ctx context.Context, t testing.TB, deps testDependencies) {
 		"ENVBUILDER_VERBOSE":                  "true",
 		"ENVBUILDER_GIT_URL":                  deps.Repo.URL,
 		"ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH": "/id_ed25519",
+		"ENVBUILDER_DOCKER_CONFIG_BASE64":     deps.DockerConfigBase64,
 	}
 
 	for k, v := range deps.ExtraEnv {
diff --git a/testutil/registrytest/registrytest.go b/testutil/registrytest/registrytest.go
index e18043a..be8d40d 100644
--- a/testutil/registrytest/registrytest.go
+++ b/testutil/registrytest/registrytest.go
@@ -2,6 +2,7 @@ package registrytest
 
 import (
 	"fmt"
+	"net/http"
 	"net/http/httptest"
 	"net/url"
 	"testing"
@@ -13,12 +14,31 @@ import (
 // New starts a new Docker registry listening on localhost.
 // It will automatically shut down when the test finishes.
 // It will store data in dir.
-func New(t testing.TB, dir string) string {
+func New(t testing.TB, dir string, mws ...func(http.Handler) http.Handler) string {
 	t.Helper()
 	regHandler := registry.New(registry.WithBlobHandler(registry.NewDiskBlobHandler(dir)))
+	for _, mw := range mws {
+		regHandler = mw(regHandler)
+	}
 	regSrv := httptest.NewServer(regHandler)
 	t.Cleanup(func() { regSrv.Close() })
 	regSrvURL, err := url.Parse(regSrv.URL)
 	require.NoError(t, err)
 	return fmt.Sprintf("localhost:%s", regSrvURL.Port())
 }
+
+func BasicAuthMW(t testing.TB, username, password string) func(http.Handler) http.Handler {
+	return func(next http.Handler) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			if username != "" || password != "" {
+				authUser, authPass, ok := r.BasicAuth()
+				if !ok || username != authUser || password != authPass {
+					t.Logf("basic auth failed: got user %q, pass %q", authUser, authPass)
+					w.WriteHeader(http.StatusUnauthorized)
+					return
+				}
+			}
+			next.ServeHTTP(w, r)
+		})
+	}
+}