From 0fa8035657edf25ebb33037180bf2767da4f6af4 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 14 Aug 2024 15:40:52 +0100 Subject: [PATCH 1/3] fix(envbuilder): RunCacheProbe: remove references to constants.MagicDir --- envbuilder.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/envbuilder.go b/envbuilder.go index 9f25481e..35268bae 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -948,7 +948,7 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error) } defaultBuildParams := func() (*devcontainer.Compiled, error) { - dockerfile := filepath.Join(constants.MagicDir, "Dockerfile") + dockerfile := filepath.Join(buildTimeWorkspaceFolder, "Dockerfile") file, err := opts.Filesystem.OpenFile(dockerfile, os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return nil, err @@ -970,7 +970,7 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error) return &devcontainer.Compiled{ DockerfilePath: dockerfile, DockerfileContent: content, - BuildContext: constants.MagicDir, + BuildContext: buildTimeWorkspaceFolder, }, nil } @@ -1010,7 +1010,7 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error) opts.Logger(log.LevelInfo, "No Dockerfile or image specified; falling back to the default image...") fallbackDockerfile = defaultParams.DockerfilePath } - buildParams, err = devContainer.Compile(opts.Filesystem, devcontainerDir, constants.MagicDir, fallbackDockerfile, opts.WorkspaceFolder, false, os.LookupEnv) + buildParams, err = devContainer.Compile(opts.Filesystem, devcontainerDir, buildTimeWorkspaceFolder, fallbackDockerfile, opts.WorkspaceFolder, false, os.LookupEnv) if err != nil { return nil, fmt.Errorf("compile devcontainer.json: %w", err) } From a8417fdffc7432f9a0b766a80b63fa635e334288 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 14 Aug 2024 17:06:16 +0100 Subject: [PATCH 2/3] chore(integration): extract gittest.CreateGitServer --- envbuilder_internal_test.go | 32 +++++ integration/integration_test.go | 200 ++++++++++++++------------------ testutil/gittest/gittest.go | 30 +++++ 3 files changed, 148 insertions(+), 114 deletions(-) diff --git a/envbuilder_internal_test.go b/envbuilder_internal_test.go index eb756071..44d5f376 100644 --- a/envbuilder_internal_test.go +++ b/envbuilder_internal_test.go @@ -1,15 +1,47 @@ package envbuilder import ( + "context" "testing" + "time" "github.com/coder/envbuilder/options" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/go-git/go-billy/v5/memfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestRunCacheProbe(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + files map[string]string + mutateOptions func(*options.Options) + assertImage func(v1.Image) + assertError func(error) + }{} { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if tc.assertError == nil && tc.assertImage == nil { + require.Failf(t, "%s: either assertError or assertImage must be defined", tc.name) + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + t.Cleanup(cancel) + var opts options.Options + img, err := RunCacheProbe(ctx, opts) + if tc.assertImage != nil { + tc.assertImage(img) + } + if tc.assertError != nil { + tc.assertError(err) + } + }) + } +} + func TestFindDevcontainerJSON(t *testing.T) { t.Parallel() diff --git a/integration/integration_test.go b/integration/integration_test.go index 297e86a1..39f8df80 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -38,7 +38,6 @@ import ( "github.com/docker/docker/api/types/volume" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" - "github.com/go-git/go-billy/v5/memfs" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/registry" @@ -68,8 +67,8 @@ func TestInitScriptInitCommand(t *testing.T) { w.WriteHeader(http.StatusOK) })) - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ // Let's say /bin/sh is not available and we can only use /bin/ash "Dockerfile": fmt.Sprintf("FROM %s\nRUN unlink /bin/sh", testImageAlpine), }, @@ -113,7 +112,7 @@ RUN mkdir -p /myapp/somedir \ && touch /myapp/somedir/somefile \ && chown 123:123 /myapp/somedir \ && chown 321:321 /myapp/somedir/somefile - + FROM %s COPY --from=builder /myapp /myapp RUN printf "%%s\n" \ @@ -127,8 +126,8 @@ RUN printf "%%s\n" \ /myapp/somedir/somefile \ > /tmp/got \ && diff -u /tmp/got /tmp/expected`, testImageAlpine, testImageAlpine) - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": dockerFile, }, }) @@ -158,8 +157,8 @@ RUN mkdir -p /myapp/somedir \ /myapp/somedir/somefile \ > /tmp/got \ && diff -u /tmp/got /tmp/expected`, testImageAlpine) - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": dockerFile, }, }) @@ -176,8 +175,8 @@ func TestForceSafe(t *testing.T) { t.Run("Safe", func(t *testing.T) { t.Parallel() - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": "FROM " + testImageAlpine, }, }) @@ -192,8 +191,8 @@ func TestForceSafe(t *testing.T) { // Careful with this one! t.Run("Unsafe", func(t *testing.T) { t.Parallel() - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": "FROM " + testImageAlpine, }, }) @@ -209,12 +208,12 @@ func TestForceSafe(t *testing.T) { func TestFailsGitAuth(t *testing.T) { t.Parallel() - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": "FROM " + testImageAlpine, }, - username: "kyle", - password: "testing", + Username: "kyle", + Password: "testing", }) _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), @@ -224,12 +223,12 @@ func TestFailsGitAuth(t *testing.T) { func TestSucceedsGitAuth(t *testing.T) { t.Parallel() - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": "FROM " + testImageAlpine, }, - username: "kyle", - password: "testing", + Username: "kyle", + Password: "testing", }) ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), @@ -244,12 +243,12 @@ func TestSucceedsGitAuth(t *testing.T) { func TestSucceedsGitAuthInURL(t *testing.T) { t.Parallel() - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": "FROM " + testImageAlpine, }, - username: "kyle", - password: "testing", + Username: "kyle", + Password: "testing", }) u, err := url.Parse(srv.URL) @@ -309,8 +308,8 @@ func TestBuildFromDevcontainerWithFeatures(t *testing.T) { require.NoError(t, err) // Ensures that a Git repository with a devcontainer.json is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ ".devcontainer/devcontainer.json": `{ "name": "Test", "build": { @@ -350,8 +349,8 @@ func TestBuildFromDevcontainerWithFeatures(t *testing.T) { func TestBuildFromDockerfile(t *testing.T) { // Ensures that a Git repository with a Dockerfile is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": "FROM " + testImageAlpine, }, }) @@ -372,8 +371,8 @@ func TestBuildFromDockerfile(t *testing.T) { func TestBuildPrintBuildOutput(t *testing.T) { // Ensures that a Git repository with a Dockerfile is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": "FROM " + testImageAlpine + "\nRUN echo hello", }, }) @@ -400,8 +399,8 @@ func TestBuildPrintBuildOutput(t *testing.T) { func TestBuildIgnoreVarRunSecrets(t *testing.T) { // Ensures that a Git repository with a Dockerfile is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": "FROM " + testImageAlpine, }, }) @@ -441,8 +440,8 @@ func TestBuildIgnoreVarRunSecrets(t *testing.T) { func TestBuildWithSetupScript(t *testing.T) { // Ensures that a Git repository with a Dockerfile is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": "FROM " + testImageAlpine, }, }) @@ -461,8 +460,8 @@ func TestBuildFromDevcontainerInCustomPath(t *testing.T) { t.Parallel() // Ensures that a Git repository with a devcontainer.json is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ ".devcontainer/custom/devcontainer.json": `{ "name": "Test", "build": { @@ -486,8 +485,8 @@ func TestBuildFromDevcontainerInSubfolder(t *testing.T) { t.Parallel() // Ensures that a Git repository with a devcontainer.json is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ ".devcontainer/subfolder/devcontainer.json": `{ "name": "Test", "build": { @@ -510,8 +509,8 @@ func TestBuildFromDevcontainerInRoot(t *testing.T) { t.Parallel() // Ensures that a Git repository with a devcontainer.json is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "devcontainer.json": `{ "name": "Test", "build": { @@ -531,11 +530,11 @@ func TestBuildFromDevcontainerInRoot(t *testing.T) { } func TestBuildCustomCertificates(t *testing.T) { - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": "FROM " + testImageAlpine, }, - tls: true, + TLS: true, }) ctr, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), @@ -553,8 +552,8 @@ func TestBuildCustomCertificates(t *testing.T) { func TestBuildStopStartCached(t *testing.T) { // Ensures that a Git repository with a Dockerfile is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": "FROM " + testImageAlpine, }, }) @@ -601,8 +600,8 @@ func TestBuildFailsFallback(t *testing.T) { t.Run("BadDockerfile", func(t *testing.T) { t.Parallel() // Ensures that a Git repository with a Dockerfile is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": "bad syntax", }, }) @@ -616,8 +615,8 @@ func TestBuildFailsFallback(t *testing.T) { t.Run("FailsBuild", func(t *testing.T) { t.Parallel() // Ensures that a Git repository with a Dockerfile is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": `FROM ` + testImageAlpine + ` RUN exit 1`, }, @@ -631,8 +630,8 @@ RUN exit 1`, t.Run("BadDevcontainer", func(t *testing.T) { t.Parallel() // Ensures that a Git repository with a Dockerfile is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ ".devcontainer/devcontainer.json": "not json", }, }) @@ -643,8 +642,8 @@ RUN exit 1`, }) t.Run("NoImageOrDockerfile", func(t *testing.T) { t.Parallel() - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ ".devcontainer/devcontainer.json": "{}", }, }) @@ -661,8 +660,8 @@ RUN exit 1`, func TestExitBuildOnFailure(t *testing.T) { t.Parallel() - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": "bad syntax", }, }) @@ -680,8 +679,8 @@ func TestContainerEnv(t *testing.T) { t.Parallel() // Ensures that a Git repository with a devcontainer.json is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ ".devcontainer/devcontainer.json": `{ "name": "Test", "build": { @@ -722,8 +721,8 @@ func TestUnsetOptionsEnv(t *testing.T) { t.Parallel() // Ensures that a Git repository with a devcontainer.json is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ ".devcontainer/devcontainer.json": `{ "name": "Test", "build": { @@ -762,8 +761,8 @@ func TestLifecycleScripts(t *testing.T) { t.Parallel() // Ensures that a Git repository with a devcontainer.json is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ ".devcontainer/devcontainer.json": `{ "name": "Test", "build": { @@ -798,8 +797,8 @@ func TestPostStartScript(t *testing.T) { t.Parallel() // Ensures that a Git repository with a devcontainer.json is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ ".devcontainer/devcontainer.json": `{ "name": "Test", "build": { @@ -848,8 +847,8 @@ func TestPrivateRegistry(t *testing.T) { }) // Ensures that a Git repository with a Dockerfile is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": "FROM " + image, }, }) @@ -867,8 +866,8 @@ func TestPrivateRegistry(t *testing.T) { }) // Ensures that a Git repository with a Dockerfile is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": "FROM " + image, }, }) @@ -899,8 +898,8 @@ func TestPrivateRegistry(t *testing.T) { }) // Ensures that a Git repository with a Dockerfile is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": "FROM " + image, }, }) @@ -1042,8 +1041,8 @@ COPY %s .`, testImageAlpine, inclFile) tc := tc t.Run(tc.name, func(t *testing.T) { - srv := createGitServer(t, gitServerOptions{ - files: tc.files, + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: tc.files, }) _, err := runEnvbuilder(t, runOpts{env: []string{ envbuilderEnv("GIT_URL", srv.URL), @@ -1066,8 +1065,8 @@ func TestPushImage(t *testing.T) { t.Run("CacheWithoutPush", func(t *testing.T) { t.Parallel() - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ ".devcontainer/Dockerfile": fmt.Sprintf(`FROM %s USER root ARG WORKDIR=/ @@ -1127,8 +1126,8 @@ RUN date --utc > /root/date.txt`, testImageAlpine), t.Run("CacheAndPush", func(t *testing.T) { t.Parallel() - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ ".devcontainer/Dockerfile": fmt.Sprintf(`FROM %s USER root ARG WORKDIR=/ @@ -1246,8 +1245,8 @@ RUN date --utc > /root/date.txt`, testImageAlpine), t.Run("CacheAndPushAuth", func(t *testing.T) { t.Parallel() - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ ".devcontainer/Dockerfile": fmt.Sprintf(`FROM %s USER root ARG WORKDIR=/ @@ -1323,8 +1322,8 @@ RUN date --utc > /root/date.txt`, testImageAlpine), t.Run("CacheAndPushAuthFail", func(t *testing.T) { t.Parallel() - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ ".devcontainer/Dockerfile": fmt.Sprintf(`FROM %s USER root ARG WORKDIR=/ @@ -1390,8 +1389,8 @@ RUN date --utc > /root/date.txt`, testImageAlpine), t.Skip("TODO: https://github.com/coder/envbuilder/issues/230") t.Parallel() - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ "Dockerfile": fmt.Sprintf(`FROM %s AS a RUN date --utc > /root/date.txt FROM %s as b @@ -1448,8 +1447,8 @@ COPY --from=a /root/date.txt /date.txt`, testImageAlpine, testImageAlpine), t.Run("PushImageRequiresCache", func(t *testing.T) { t.Parallel() - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ ".devcontainer/Dockerfile": fmt.Sprintf(`FROM %s USER root ARG WORKDIR=/ @@ -1480,8 +1479,8 @@ RUN date --utc > /root/date.txt`, testImageAlpine), t.Run("PushErr", func(t *testing.T) { t.Parallel() - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ ".devcontainer/Dockerfile": fmt.Sprintf(`FROM %s USER root ARG WORKDIR=/ @@ -1518,8 +1517,8 @@ func TestChownHomedir(t *testing.T) { t.Parallel() // Ensures that a Git repository with a devcontainer.json is cloned and built. - srv := createGitServer(t, gitServerOptions{ - files: map[string]string{ + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ ".devcontainer/devcontainer.json": `{ "name": "Test", "build": { @@ -1591,33 +1590,6 @@ func TestMain(m *testing.M) { m.Run() } -type gitServerOptions struct { - files map[string]string - username string - password string - authMW func(http.Handler) http.Handler - tls bool -} - -// createGitServer creates a git repository with an in-memory filesystem -// and serves it over HTTP using a httptest.Server. -func createGitServer(t *testing.T, opts gitServerOptions) *httptest.Server { - t.Helper() - if opts.authMW == nil { - opts.authMW = mwtest.BasicAuthMW(opts.username, opts.password) - } - commits := make([]gittest.CommitFunc, 0) - for path, content := range opts.files { - commits = append(commits, gittest.Commit(t, path, content, "my test commit")) - } - fs := memfs.New() - _ = gittest.NewRepo(t, fs, commits...) - if opts.tls { - return httptest.NewTLSServer(opts.authMW(gittest.NewServer(fs))) - } - return httptest.NewServer(opts.authMW(gittest.NewServer(fs))) -} - func checkTestRegistry() { resp, err := http.Get("http://localhost:5000/v2/_catalog") if err != nil { diff --git a/testutil/gittest/gittest.go b/testutil/gittest/gittest.go index ffa9bd01..f3d5f1d3 100644 --- a/testutil/gittest/gittest.go +++ b/testutil/gittest/gittest.go @@ -6,6 +6,7 @@ import ( "log" "net" "net/http" + "net/http/httptest" "os" "os/exec" "sync" @@ -14,8 +15,10 @@ import ( gossh "golang.org/x/crypto/ssh" + "github.com/coder/envbuilder/testutil/mwtest" "github.com/gliderlabs/ssh" "github.com/go-git/go-billy/v5" + "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/cache" @@ -28,6 +31,33 @@ import ( "github.com/stretchr/testify/require" ) +type Options struct { + Files map[string]string + Username string + Password string + AuthMW func(http.Handler) http.Handler + TLS bool +} + +// CreateGitServer creates a git repository with an in-memory filesystem +// and serves it over HTTP using a httptest.Server. +func CreateGitServer(t *testing.T, opts Options) *httptest.Server { + t.Helper() + if opts.AuthMW == nil { + opts.AuthMW = mwtest.BasicAuthMW(opts.Username, opts.Password) + } + commits := make([]CommitFunc, 0) + for path, content := range opts.Files { + commits = append(commits, Commit(t, path, content, "my test commit")) + } + fs := memfs.New() + _ = NewRepo(t, fs, commits...) + if opts.TLS { + return httptest.NewTLSServer(opts.AuthMW(NewServer(fs))) + } + return httptest.NewServer(opts.AuthMW(NewServer(fs))) +} + // NewServer returns a http.Handler that serves a git repository. // It's expected that the repository is already initialized by the caller. func NewServer(fs billy.Filesystem) http.Handler { From 1e5fc4533e6244d5bf30fbdd7b98476f58b2f036 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 14 Aug 2024 17:23:42 +0100 Subject: [PATCH 3/3] chore(integration): add test for cache probe with devcontainer only --- envbuilder_internal_test.go | 32 ---------- integration/integration_test.go | 106 ++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 32 deletions(-) diff --git a/envbuilder_internal_test.go b/envbuilder_internal_test.go index 44d5f376..eb756071 100644 --- a/envbuilder_internal_test.go +++ b/envbuilder_internal_test.go @@ -1,47 +1,15 @@ package envbuilder import ( - "context" "testing" - "time" "github.com/coder/envbuilder/options" - v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/go-git/go-billy/v5/memfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestRunCacheProbe(t *testing.T) { - t.Parallel() - - for _, tc := range []struct { - name string - files map[string]string - mutateOptions func(*options.Options) - assertImage func(v1.Image) - assertError func(error) - }{} { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - if tc.assertError == nil && tc.assertImage == nil { - require.Failf(t, "%s: either assertError or assertImage must be defined", tc.name) - } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - t.Cleanup(cancel) - var opts options.Options - img, err := RunCacheProbe(ctx, opts) - if tc.assertImage != nil { - tc.assertImage(img) - } - if tc.assertError != nil { - tc.assertError(err) - } - }) - } -} - func TestFindDevcontainerJSON(t *testing.T) { t.Parallel() diff --git a/integration/integration_test.go b/integration/integration_test.go index 39f8df80..af051473 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -1242,6 +1242,112 @@ RUN date --utc > /root/date.txt`, testImageAlpine), require.NotEmpty(t, strings.TrimSpace(out)) }) + t.Run("CacheAndPushDevcontainerOnly", func(t *testing.T) { + t.Parallel() + + srv := gittest.CreateGitServer(t, gittest.Options{ + Files: map[string]string{ + ".devcontainer/devcontainer.json": fmt.Sprintf(`{"image": %q}`, testImageAlpine), + }, + }) + + // Given: an empty registry + testReg := setupInMemoryRegistry(t, setupInMemoryRegistryOpts{}) + testRepo := testReg + "/test" + ref, err := name.ParseReference(testRepo + ":latest") + require.NoError(t, err) + _, err = remote.Image(ref) + require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push") + + // When: we run envbuilder with GET_CACHED_IMAGE + _, err = runEnvbuilder(t, runOpts{env: []string{ + envbuilderEnv("GIT_URL", srv.URL), + envbuilderEnv("CACHE_REPO", testRepo), + envbuilderEnv("GET_CACHED_IMAGE", "1"), + }}) + require.ErrorContains(t, err, "error probing build cache: uncached COPY command") + // Then: it should fail to build the image and nothing should be pushed + _, err = remote.Image(ref) + require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push") + + // When: we run envbuilder with PUSH_IMAGE set + _, err = runEnvbuilder(t, runOpts{env: []string{ + envbuilderEnv("GIT_URL", srv.URL), + envbuilderEnv("CACHE_REPO", testRepo), + envbuilderEnv("PUSH_IMAGE", "1"), + }}) + require.NoError(t, err) + + // Then: the image should be pushed + img, err := remote.Image(ref) + require.NoError(t, err, "expected image to be present after build + push") + + // Then: the image should have its directives replaced with those required + // to run envbuilder automatically + configFile, err := img.ConfigFile() + require.NoError(t, err, "expected image to return a config file") + + assert.Equal(t, "root", configFile.Config.User, "user must be root") + assert.Equal(t, "/", configFile.Config.WorkingDir, "workdir must be /") + if assert.Len(t, configFile.Config.Entrypoint, 1) { + assert.Equal(t, "/.envbuilder/bin/envbuilder", configFile.Config.Entrypoint[0], "incorrect entrypoint") + } + + // Then: re-running envbuilder with GET_CACHED_IMAGE should succeed + ctrID, err := runEnvbuilder(t, runOpts{env: []string{ + envbuilderEnv("GIT_URL", srv.URL), + envbuilderEnv("CACHE_REPO", testRepo), + envbuilderEnv("GET_CACHED_IMAGE", "1"), + }}) + require.NoError(t, err) + + // Then: the cached image ref should be emitted in the container logs + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + require.NoError(t, err) + defer cli.Close() + logs, err := cli.ContainerLogs(ctx, ctrID, container.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + }) + require.NoError(t, err) + defer logs.Close() + logBytes, err := io.ReadAll(logs) + require.NoError(t, err) + require.Regexp(t, `ENVBUILDER_CACHED_IMAGE=(\S+)`, string(logBytes)) + + // When: we pull the image we just built + rc, err := cli.ImagePull(ctx, ref.String(), image.PullOptions{}) + require.NoError(t, err) + t.Cleanup(func() { _ = rc.Close() }) + _, err = io.ReadAll(rc) + require.NoError(t, err) + + // When: we run the image we just built + ctr, err := cli.ContainerCreate(ctx, &container.Config{ + Image: ref.String(), + Entrypoint: []string{"sleep", "infinity"}, + Labels: map[string]string{ + testContainerLabel: "true", + }, + }, nil, nil, nil, "") + require.NoError(t, err) + t.Cleanup(func() { + _ = cli.ContainerRemove(ctx, ctr.ID, container.RemoveOptions{ + RemoveVolumes: true, + Force: true, + }) + }) + err = cli.ContainerStart(ctx, ctr.ID, container.StartOptions{}) + require.NoError(t, err) + + // Then: the envbuilder binary exists in the image! + out := execContainer(t, ctr.ID, "/.envbuilder/bin/envbuilder --help") + require.Regexp(t, `(?s)^USAGE:\s+envbuilder`, strings.TrimSpace(out)) + require.NotEmpty(t, strings.TrimSpace(out)) + }) + t.Run("CacheAndPushAuth", func(t *testing.T) { t.Parallel()