diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 86603f2e..1407f3c8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -48,4 +48,4 @@ jobs: go-version: "~1.21" - name: Test - run: go test ./... + run: make test diff --git a/.gitignore b/.gitignore index 001aeac3..1636db2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ scripts/envbuilder-* +.registry-cache \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..33a4ddbb --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +GOARCH := $(shell go env GOARCH) +PWD=$(shell pwd) + +develop: + ./scripts/develop.sh + +build: scripts/envbuilder-$(GOARCH) + ./scripts/build.sh + +.PHONY: test +test: test-registry test-images + go test -count=1 ./... + +# Starts a local Docker registry on port 5000 with a local disk cache. +.PHONY: test-registry +test-registry: .registry-cache + if ! curl -fsSL http://localhost:5000/v2/_catalog > /dev/null 2>&1; then \ + docker rm -f envbuilder-registry && \ + docker run -d -p 5000:5000 --name envbuilder-registry --volume $(PWD)/.registry-cache:/var/lib/registry registry:2; \ + fi + +# Pulls images referenced in integration tests and pushes them to the local cache. +.PHONY: test-images +test-images: .registry-cache .registry-cache/docker/registry/v2/repositories/envbuilder-test-alpine .registry-cache/docker/registry/v2/repositories/envbuilder-test-ubuntu .registry-cache/docker/registry/v2/repositories/envbuilder-test-codercom-code-server + +.registry-cache: + mkdir -p .registry-cache && chmod -R ag+w .registry-cache + +.registry-cache/docker/registry/v2/repositories/envbuilder-test-alpine: + docker pull alpine:latest + docker tag alpine:latest localhost:5000/envbuilder-test-alpine:latest + docker push localhost:5000/envbuilder-test-alpine:latest + +.registry-cache/docker/registry/v2/repositories/envbuilder-test-ubuntu: + docker pull ubuntu:latest + docker tag ubuntu:latest localhost:5000/envbuilder-test-ubuntu:latest + docker push localhost:5000/envbuilder-test-ubuntu:latest + +.registry-cache/docker/registry/v2/repositories/envbuilder-test-codercom-code-server: + docker pull codercom/code-server:latest + docker tag codercom/code-server:latest localhost:5000/envbuilder-test-codercom-code-server:latest + docker push localhost:5000/envbuilder-test-codercom-code-server:latest \ No newline at end of file diff --git a/README.md b/README.md index 61718e22..05a43631 100644 --- a/README.md +++ b/README.md @@ -224,3 +224,23 @@ docker run -it --rm \ - [`SSL_CERT_FILE`](https://go.dev/src/crypto/x509/root_unix.go#L19): Specifies the path to an SSL certificate. - [`SSL_CERT_DIR`](https://go.dev/src/crypto/x509/root_unix.go#L25): Identifies which directory to check for SSL certificate files. - `SSL_CERT_BASE64`: Specifies a base64-encoded SSL certificate that will be added to the global certificate pool on start. + + +# Local Development + +Building `envbuilder` currently **requires** a Linux system. + +On MacOS or Windows systems, we recommend either using a VM or the provided `.devcontainer` for development. + +**Additional Requirements:** + +- `go 1.21` +- `make` +- Docker daemon (for running tests) + +**Makefile targets:** + +- `build`: builds and tags `envbuilder:latest` for your current architecture. +- `develop`: runs `envbuilder:latest` against a sample Git repository. +- `test`: run tests. +- `test-registry`: stands up a local registry for caching images used in tests. \ No newline at end of file diff --git a/devcontainer/devcontainer_test.go b/devcontainer/devcontainer_test.go index 13054c3f..d2276d35 100644 --- a/devcontainer/devcontainer_test.go +++ b/devcontainer/devcontainer_test.go @@ -78,7 +78,7 @@ func TestCompileWithFeatures(t *testing.T) { "context": ".", }, // Comments here! - "image": "codercom/code-server:latest", + "image": "localhost:5000/envbuilder-test-codercom-code-server:latest", "features": { "` + featureOne + `": {}, "` + featureTwo + `": "potato" @@ -96,7 +96,7 @@ func TestCompileWithFeatures(t *testing.T) { featureTwoMD5 := md5.Sum([]byte(featureTwo)) featureTwoDir := fmt.Sprintf("/.envbuilder/features/two-%x", featureTwoMD5[:4]) - require.Equal(t, `FROM codercom/code-server:latest + require.Equal(t, `FROM localhost:5000/envbuilder-test-codercom-code-server:latest USER root # Rust tomato - Example description! @@ -116,7 +116,7 @@ func TestCompileDevContainer(t *testing.T) { t.Parallel() fs := memfs.New() dc := &devcontainer.Spec{ - Image: "codercom/code-server:latest", + Image: "localhost:5000/envbuilder-test-ubuntu:latest", } params, err := dc.Compile(fs, "", magicDir, "", "", false) require.NoError(t, err) @@ -141,7 +141,7 @@ func TestCompileDevContainer(t *testing.T) { require.NoError(t, err) file, err := fs.OpenFile(filepath.Join(dcDir, "Dockerfile"), os.O_CREATE|os.O_WRONLY, 0644) require.NoError(t, err) - _, err = io.WriteString(file, "FROM ubuntu") + _, err = io.WriteString(file, "FROM localhost:5000/envbuilder-test-ubuntu:latest") require.NoError(t, err) _ = file.Close() params, err := dc.Compile(fs, dcDir, magicDir, "", "/var/workspace", false) diff --git a/integration/integration_test.go b/integration/integration_test.go index c3123032..ba63ac6f 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -43,13 +43,15 @@ import ( const ( testContainerLabel = "envbox-integration-test" + testImageAlpine = "localhost:5000/envbuilder-test-alpine:latest" + testImageUbuntu = "localhost:5000/envbuilder-test-ubuntu:latest" ) func TestFailsGitAuth(t *testing.T) { t.Parallel() url := createGitServer(t, gitServerOptions{ files: map[string]string{ - "Dockerfile": "FROM alpine:latest", + "Dockerfile": "FROM " + testImageAlpine, }, username: "kyle", password: "testing", @@ -64,7 +66,7 @@ func TestSucceedsGitAuth(t *testing.T) { t.Parallel() url := createGitServer(t, gitServerOptions{ files: map[string]string{ - "Dockerfile": "FROM alpine:latest", + "Dockerfile": "FROM " + testImageAlpine, }, username: "kyle", password: "testing", @@ -142,7 +144,7 @@ func TestBuildFromDevcontainerWithFeatures(t *testing.T) { } } }`, - ".devcontainer/Dockerfile": "FROM ubuntu", + ".devcontainer/Dockerfile": "FROM " + testImageUbuntu, ".devcontainer/feature3/devcontainer-feature.json": string(feature3Spec), ".devcontainer/feature3/install.sh": "echo $GRAPE > /test3output", }, @@ -166,7 +168,7 @@ func TestBuildFromDockerfile(t *testing.T) { // Ensures that a Git repository with a Dockerfile is cloned and built. url := createGitServer(t, gitServerOptions{ files: map[string]string{ - "Dockerfile": "FROM alpine:latest", + "Dockerfile": "FROM " + testImageAlpine, }, }) ctr, err := runEnvbuilder(t, options{env: []string{ @@ -183,7 +185,7 @@ func TestBuildPrintBuildOutput(t *testing.T) { // Ensures that a Git repository with a Dockerfile is cloned and built. url := createGitServer(t, gitServerOptions{ files: map[string]string{ - "Dockerfile": "FROM alpine:latest\nRUN echo hello", + "Dockerfile": "FROM " + testImageAlpine + "\nRUN echo hello", }, }) ctr, err := runEnvbuilder(t, options{env: []string{ @@ -211,7 +213,7 @@ func TestBuildIgnoreVarRunSecrets(t *testing.T) { // Ensures that a Git repository with a Dockerfile is cloned and built. url := createGitServer(t, gitServerOptions{ files: map[string]string{ - "Dockerfile": "FROM alpine:latest", + "Dockerfile": "FROM " + testImageAlpine, }, }) dir := t.TempDir() @@ -234,7 +236,7 @@ func TestBuildWithSetupScript(t *testing.T) { // Ensures that a Git repository with a Dockerfile is cloned and built. url := createGitServer(t, gitServerOptions{ files: map[string]string{ - "Dockerfile": "FROM alpine:latest", + "Dockerfile": "FROM " + testImageAlpine, }, }) ctr, err := runEnvbuilder(t, options{env: []string{ @@ -260,7 +262,7 @@ func TestBuildFromDevcontainerInCustomPath(t *testing.T) { "dockerfile": "Dockerfile" }, }`, - ".devcontainer/custom/Dockerfile": "FROM ubuntu", + ".devcontainer/custom/Dockerfile": "FROM " + testImageUbuntu, }, }) ctr, err := runEnvbuilder(t, options{env: []string{ @@ -285,7 +287,7 @@ func TestBuildFromDevcontainerInSubfolder(t *testing.T) { "dockerfile": "Dockerfile" }, }`, - ".devcontainer/subfolder/Dockerfile": "FROM ubuntu", + ".devcontainer/subfolder/Dockerfile": "FROM " + testImageUbuntu, }, }) ctr, err := runEnvbuilder(t, options{env: []string{ @@ -308,7 +310,7 @@ func TestBuildFromDevcontainerInRoot(t *testing.T) { "dockerfile": "Dockerfile" }, }`, - "Dockerfile": "FROM ubuntu", + "Dockerfile": "FROM " + testImageUbuntu, }, }) ctr, err := runEnvbuilder(t, options{env: []string{ @@ -323,7 +325,7 @@ func TestBuildFromDevcontainerInRoot(t *testing.T) { func TestBuildCustomCertificates(t *testing.T) { srv := httptest.NewTLSServer(createGitHandler(t, gitServerOptions{ files: map[string]string{ - "Dockerfile": "FROM alpine:latest", + "Dockerfile": "FROM " + testImageAlpine, }, })) ctr, err := runEnvbuilder(t, options{env: []string{ @@ -344,7 +346,7 @@ func TestBuildStopStartCached(t *testing.T) { // Ensures that a Git repository with a Dockerfile is cloned and built. url := createGitServer(t, gitServerOptions{ files: map[string]string{ - "Dockerfile": "FROM alpine:latest", + "Dockerfile": "FROM " + testImageAlpine, }, }) ctr, err := runEnvbuilder(t, options{env: []string{ @@ -407,7 +409,7 @@ func TestBuildFailsFallback(t *testing.T) { // Ensures that a Git repository with a Dockerfile is cloned and built. url := createGitServer(t, gitServerOptions{ files: map[string]string{ - "Dockerfile": `FROM alpine + "Dockerfile": `FROM ` + testImageAlpine + ` RUN exit 1`, }, }) @@ -439,7 +441,7 @@ RUN exit 1`, }) ctr, err := runEnvbuilder(t, options{env: []string{ "GIT_URL=" + url, - "FALLBACK_IMAGE=alpine:latest", + "FALLBACK_IMAGE=" + testImageAlpine, }}) require.NoError(t, err) @@ -458,7 +460,7 @@ func TestExitBuildOnFailure(t *testing.T) { _, err := runEnvbuilder(t, options{env: []string{ "GIT_URL=" + url, "DOCKERFILE_PATH=Dockerfile", - "FALLBACK_IMAGE=alpine", + "FALLBACK_IMAGE=" + testImageAlpine, // Ensures that the fallback doesn't work when an image is specified. "EXIT_ON_BUILD_FAILURE=true", }}) @@ -486,7 +488,7 @@ func TestContainerEnv(t *testing.T) { "REMOTE_BAR": "${FROM_CONTAINER_ENV}" } }`, - ".devcontainer/Dockerfile": "FROM alpine:latest\nENV FROM_DOCKERFILE=foo", + ".devcontainer/Dockerfile": "FROM " + testImageAlpine + "\nENV FROM_DOCKERFILE=foo", }, }) ctr, err := runEnvbuilder(t, options{env: []string{ @@ -523,7 +525,7 @@ func TestLifecycleScripts(t *testing.T) { "parallel2": ["sh", "-c", "echo parallel2 > /tmp/parallel2"] } }`, - ".devcontainer/Dockerfile": "FROM alpine:latest\nUSER nobody", + ".devcontainer/Dockerfile": "FROM " + testImageAlpine + "\nUSER nobody", }, }) ctr, err := runEnvbuilder(t, options{env: []string{ @@ -559,7 +561,7 @@ func TestPostStartScript(t *testing.T) { ".devcontainer/init.sh": `#!/bin/sh /tmp/post-start.sh sleep infinity`, - ".devcontainer/Dockerfile": `FROM alpine:latest + ".devcontainer/Dockerfile": `FROM ` + testImageAlpine + ` COPY init.sh /bin RUN chmod +x /bin/init.sh USER nobody`, @@ -586,7 +588,9 @@ func TestPrivateRegistry(t *testing.T) { t.Parallel() t.Run("NoAuth", func(t *testing.T) { t.Parallel() - image := setupPassthroughRegistry(t, "library/alpine", ®istryAuth{ + // Even if something goes wrong with auth, + // the pull will fail as "scratch" is a reserved name. + image := setupPassthroughRegistry(t, "scratch", ®istryAuth{ Username: "user", Password: "test", }) @@ -605,7 +609,7 @@ func TestPrivateRegistry(t *testing.T) { }) t.Run("Auth", func(t *testing.T) { t.Parallel() - image := setupPassthroughRegistry(t, "library/alpine", ®istryAuth{ + image := setupPassthroughRegistry(t, "envbuilder-test-alpine:latest", ®istryAuth{ Username: "user", Password: "test", }) @@ -635,7 +639,9 @@ func TestPrivateRegistry(t *testing.T) { }) t.Run("InvalidAuth", func(t *testing.T) { t.Parallel() - image := setupPassthroughRegistry(t, "library/alpine", ®istryAuth{ + // Even if something goes wrong with auth, + // the pull will fail as "scratch" is a reserved name. + image := setupPassthroughRegistry(t, "scratch", ®istryAuth{ Username: "user", Password: "banana", }) @@ -672,22 +678,22 @@ type registryAuth struct { func setupPassthroughRegistry(t *testing.T, image string, auth *registryAuth) string { t.Helper() - dockerURL, err := url.Parse("https://registry-1.docker.io") + dockerURL, err := url.Parse("http://localhost:5000") require.NoError(t, err) proxy := httputil.NewSingleHostReverseProxy(dockerURL) // The Docker registry uses short-lived JWTs to authenticate // anonymously to pull images. To test our MITM auth, we need to // generate a JWT for the proxy to use. - registry, err := name.NewRegistry("registry-1.docker.io") + registry, err := name.NewRegistry("localhost:5000") require.NoError(t, err) proxy.Transport, err = transport.NewWithContext(context.Background(), registry, authn.Anonymous, http.DefaultTransport, []string{}) require.NoError(t, err) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - r.Host = "registry-1.docker.io" - r.URL.Host = "registry-1.docker.io" - r.URL.Scheme = "https" + r.Host = "localhost:5000" + r.URL.Host = "localhost:5000" + r.URL.Scheme = "http" if auth != nil { user, pass, ok := r.BasicAuth()