Skip to content

Commit f723785

Browse files
committed
Merge branch 'main' of github.com:coder/envbuilder into bq/docs
2 parents 15e00e2 + 9685dcc commit f723785

File tree

10 files changed

+701
-264
lines changed

10 files changed

+701
-264
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,4 @@ jobs:
4848
go-version: "~1.21"
4949

5050
- name: Test
51-
run: go test ./...
51+
run: make test

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
scripts/envbuilder-*
2+
.registry-cache

Makefile

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
GOARCH := $(shell go env GOARCH)
2+
PWD=$(shell pwd)
3+
4+
develop:
5+
./scripts/develop.sh
6+
7+
build: scripts/envbuilder-$(GOARCH)
8+
./scripts/build.sh
9+
10+
.PHONY: test
11+
test: test-registry test-images
12+
go test -count=1 ./...
13+
14+
test-race:
15+
go test -race -count=3 ./...
16+
17+
# Starts a local Docker registry on port 5000 with a local disk cache.
18+
.PHONY: test-registry
19+
test-registry: .registry-cache
20+
if ! curl -fsSL http://localhost:5000/v2/_catalog > /dev/null 2>&1; then \
21+
docker rm -f envbuilder-registry && \
22+
docker run -d -p 5000:5000 --name envbuilder-registry --volume $(PWD)/.registry-cache:/var/lib/registry registry:2; \
23+
fi
24+
25+
# Pulls images referenced in integration tests and pushes them to the local cache.
26+
.PHONY: test-images
27+
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
28+
29+
.registry-cache:
30+
mkdir -p .registry-cache && chmod -R ag+w .registry-cache
31+
32+
.registry-cache/docker/registry/v2/repositories/envbuilder-test-alpine:
33+
docker pull alpine:latest
34+
docker tag alpine:latest localhost:5000/envbuilder-test-alpine:latest
35+
docker push localhost:5000/envbuilder-test-alpine:latest
36+
37+
.registry-cache/docker/registry/v2/repositories/envbuilder-test-ubuntu:
38+
docker pull ubuntu:latest
39+
docker tag ubuntu:latest localhost:5000/envbuilder-test-ubuntu:latest
40+
docker push localhost:5000/envbuilder-test-ubuntu:latest
41+
42+
.registry-cache/docker/registry/v2/repositories/envbuilder-test-codercom-code-server:
43+
docker pull codercom/code-server:latest
44+
docker tag codercom/code-server:latest localhost:5000/envbuilder-test-codercom-code-server:latest
45+
docker push localhost:5000/envbuilder-test-codercom-code-server:latest

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,23 @@ docker run -it --rm \
224224
- [`SSL_CERT_FILE`](https://go.dev/src/crypto/x509/root_unix.go#L19): Specifies the path to an SSL certificate.
225225
- [`SSL_CERT_DIR`](https://go.dev/src/crypto/x509/root_unix.go#L25): Identifies which directory to check for SSL certificate files.
226226
- `SSL_CERT_BASE64`: Specifies a base64-encoded SSL certificate that will be added to the global certificate pool on start.
227+
228+
229+
# Local Development
230+
231+
Building `envbuilder` currently **requires** a Linux system.
232+
233+
On MacOS or Windows systems, we recommend either using a VM or the provided `.devcontainer` for development.
234+
235+
**Additional Requirements:**
236+
237+
- `go 1.21`
238+
- `make`
239+
- Docker daemon (for running tests)
240+
241+
**Makefile targets:**
242+
243+
- `build`: builds and tags `envbuilder:latest` for your current architecture.
244+
- `develop`: runs `envbuilder:latest` against a sample Git repository.
245+
- `test`: run tests.
246+
- `test-registry`: stands up a local registry for caching images used in tests.

devcontainer/devcontainer_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func TestCompileWithFeatures(t *testing.T) {
7878
"context": ".",
7979
},
8080
// Comments here!
81-
"image": "codercom/code-server:latest",
81+
"image": "localhost:5000/envbuilder-test-codercom-code-server:latest",
8282
"features": {
8383
"` + featureOne + `": {},
8484
"` + featureTwo + `": "potato"
@@ -96,7 +96,7 @@ func TestCompileWithFeatures(t *testing.T) {
9696
featureTwoMD5 := md5.Sum([]byte(featureTwo))
9797
featureTwoDir := fmt.Sprintf("/.envbuilder/features/two-%x", featureTwoMD5[:4])
9898

99-
require.Equal(t, `FROM codercom/code-server:latest
99+
require.Equal(t, `FROM localhost:5000/envbuilder-test-codercom-code-server:latest
100100
101101
USER root
102102
# Rust tomato - Example description!
@@ -116,7 +116,7 @@ func TestCompileDevContainer(t *testing.T) {
116116
t.Parallel()
117117
fs := memfs.New()
118118
dc := &devcontainer.Spec{
119-
Image: "codercom/code-server:latest",
119+
Image: "localhost:5000/envbuilder-test-ubuntu:latest",
120120
}
121121
params, err := dc.Compile(fs, "", magicDir, "", "", false)
122122
require.NoError(t, err)
@@ -141,7 +141,7 @@ func TestCompileDevContainer(t *testing.T) {
141141
require.NoError(t, err)
142142
file, err := fs.OpenFile(filepath.Join(dcDir, "Dockerfile"), os.O_CREATE|os.O_WRONLY, 0644)
143143
require.NoError(t, err)
144-
_, err = io.WriteString(file, "FROM ubuntu")
144+
_, err = io.WriteString(file, "FROM localhost:5000/envbuilder-test-ubuntu:latest")
145145
require.NoError(t, err)
146146
_ = file.Close()
147147
params, err := dc.Compile(fs, dcDir, magicDir, "", "/var/workspace", false)

envbuilder.go

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -267,22 +267,11 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error {
267267
if options.GetString("DockerFilepath") == "" {
268268
// Only look for a devcontainer if a Dockerfile wasn't specified.
269269
// devcontainer is a standard, so it's reasonable to be the default.
270-
devcontainerDir := options.GetString("DevcontainerDir")
271-
if devcontainerDir == "" {
272-
devcontainerDir = ".devcontainer"
273-
}
274-
if !filepath.IsAbs(devcontainerDir) {
275-
devcontainerDir = filepath.Join(options.GetString("WorkspaceFolder"), devcontainerDir)
276-
}
277-
devcontainerPath := options.GetString("DevcontainerJSONPath")
278-
if devcontainerPath == "" {
279-
devcontainerPath = "devcontainer.json"
280-
}
281-
if !filepath.IsAbs(devcontainerPath) {
282-
devcontainerPath = filepath.Join(devcontainerDir, devcontainerPath)
283-
}
284-
_, err := deps.Filesystem.Stat(devcontainerPath)
285-
if err == nil {
270+
devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&options, &deps)
271+
if err != nil {
272+
logf(codersdk.LogLevelError, "Failed to locate devcontainer.json: %s", err.Error())
273+
logf(codersdk.LogLevelError, "Falling back to the default image...")
274+
} else {
286275
// We know a devcontainer exists.
287276
// Let's parse it and use it!
288277
file, err := deps.Filesystem.Open(devcontainerPath)
@@ -317,7 +306,16 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error {
317306
}
318307
} else {
319308
// If a Dockerfile was specified, we use that.
320-
dockerfilePath := filepath.Join(options.GetString("WorkspaceFolder"), options.GetString("DockerFilepath"))
309+
dockerfilePath := filepath.Join(options.GetString("WorkspaceFolder"), options.GetString("DockerfilePath"))
310+
311+
// If the dockerfilePath is specified and deeper than the base of WorkspaceFolder AND the BuildContextPath is
312+
// not defined, show a warning
313+
dockerfileDir := filepath.Dir(dockerfilePath)
314+
if dockerfileDir != filepath.Clean(options.GetString("WorkspaceFolder")) && options.GetString("BuildContextPath") == "" {
315+
logf(codersdk.LogLevelWarn, "given dockerfile %q is below %q and no custom build context has been defined", dockerfilePath, options.GetString("WorkspaceFolder"))
316+
logf(codersdk.LogLevelWarn, "\t-> set BUILD_CONTEXT_PATH to %q to fix", dockerfileDir)
317+
}
318+
321319
dockerfile, err := deps.Filesystem.Open(dockerfilePath)
322320
if err == nil {
323321
content, err := io.ReadAll(dockerfile)
@@ -327,7 +325,7 @@ func Run(ctx context.Context, options OptionsMap, deps Dependencies) error {
327325
buildParams = &devcontainer.Compiled{
328326
DockerfilePath: dockerfilePath,
329327
DockerfileContent: string(content),
330-
BuildContext: options.GetString("WorkspaceFolder"),
328+
BuildContext: filepath.Join(options.GetString("WorkspaceFolder"), options.GetString("BuildContextPath")),
331329
}
332330
}
333331
}
@@ -1002,3 +1000,71 @@ type osfsWithChmod struct {
10021000
func (fs *osfsWithChmod) Chmod(name string, mode os.FileMode) error {
10031001
return os.Chmod(name, mode)
10041002
}
1003+
1004+
func findDevcontainerJSON(options *OptionsMap, deps *Dependencies) (string, string, error) {
1005+
// 0. Check if custom devcontainer directory or path is provided.
1006+
if options.GetString("DevcontainerDir") != "" || options.GetString("DevcontainerJSONPath") != "" {
1007+
devcontainerDir := options.GetString("DevcontainerDir")
1008+
if devcontainerDir == "" {
1009+
devcontainerDir = ".devcontainer"
1010+
}
1011+
1012+
// If `devcontainerDir` is not an absolute path, assume it is relative to the workspace folder.
1013+
if !filepath.IsAbs(devcontainerDir) {
1014+
devcontainerDir = filepath.Join(options.GetString("WorkspaceFolder"), devcontainerDir)
1015+
}
1016+
1017+
// An absolute location always takes a precedence.
1018+
devcontainerPath := options.GetString("DevcontainerJSONPath")
1019+
if filepath.IsAbs(devcontainerPath) {
1020+
return options.GetString("DevcontainerJSONPath"), devcontainerDir, nil
1021+
}
1022+
// If an override is not provided, assume it is just `devcontainer.json`.
1023+
if devcontainerPath == "" {
1024+
devcontainerPath = "devcontainer.json"
1025+
}
1026+
1027+
if !filepath.IsAbs(devcontainerPath) {
1028+
devcontainerPath = filepath.Join(devcontainerDir, devcontainerPath)
1029+
}
1030+
return devcontainerPath, devcontainerDir, nil
1031+
}
1032+
1033+
// 1. Check `options.WorkspaceFolder`/.devcontainer/devcontainer.json.
1034+
location := filepath.Join(options.GetString("WorkspaceFolder"), ".devcontainer", "devcontainer.json")
1035+
if _, err := deps.Filesystem.Stat(location); err == nil {
1036+
return location, filepath.Dir(location), nil
1037+
}
1038+
1039+
// 2. Check `options.WorkspaceFolder`/devcontainer.json.
1040+
location = filepath.Join(options.GetString("WorkspaceFolder"), "devcontainer.json")
1041+
if _, err := deps.Filesystem.Stat(location); err == nil {
1042+
return location, filepath.Dir(location), nil
1043+
}
1044+
1045+
// 3. Check every folder: `options.WorkspaceFolder`/.devcontainer/<folder>/devcontainer.json.
1046+
devcontainerDir := filepath.Join(options.GetString("WorkspaceFolder"), ".devcontainer")
1047+
1048+
fileInfos, err := deps.Filesystem.ReadDir(devcontainerDir)
1049+
if err != nil {
1050+
return "", "", err
1051+
}
1052+
1053+
logf := deps.Logger
1054+
for _, fileInfo := range fileInfos {
1055+
if !fileInfo.IsDir() {
1056+
logf(codersdk.LogLevelDebug, `%s is a file`, fileInfo.Name())
1057+
continue
1058+
}
1059+
1060+
location := filepath.Join(devcontainerDir, fileInfo.Name(), "devcontainer.json")
1061+
if _, err := deps.Filesystem.Stat(location); err != nil {
1062+
logf(codersdk.LogLevelDebug, `stat %s failed: %s`, location, err.Error())
1063+
continue
1064+
}
1065+
1066+
return location, filepath.Dir(location), nil
1067+
}
1068+
1069+
return "", "", errors.New("can't find devcontainer.json, is it a correct spec?")
1070+
}

envbuilder_internal_test.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package envbuilder
2+
3+
import (
4+
"testing"
5+
6+
"github.com/go-git/go-billy/v5/memfs"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestFindDevcontainerJSON(t *testing.T) {
12+
t.Parallel()
13+
14+
t.Run("empty filesystem", func(t *testing.T) {
15+
t.Parallel()
16+
17+
// given
18+
fs := memfs.New()
19+
20+
// when
21+
options := DefaultOptions()
22+
options.SetString("WorkspaceFolder", "/workspace")
23+
_, _, err := findDevcontainerJSON(&options, &Dependencies{Filesystem: fs})
24+
25+
// then
26+
require.Error(t, err)
27+
})
28+
29+
t.Run("devcontainers.json is missing", func(t *testing.T) {
30+
t.Parallel()
31+
32+
// given
33+
fs := memfs.New()
34+
err := fs.MkdirAll("/workspace/.devcontainer", 0600)
35+
require.NoError(t, err)
36+
37+
// when
38+
options := DefaultOptions()
39+
options.SetString("WorkspaceFolder", "/workspace")
40+
_, _, err = findDevcontainerJSON(&options, &Dependencies{Filesystem: fs})
41+
42+
// then
43+
require.Error(t, err)
44+
})
45+
46+
t.Run("default configuration", func(t *testing.T) {
47+
t.Parallel()
48+
49+
// given
50+
fs := memfs.New()
51+
err := fs.MkdirAll("/workspace/.devcontainer", 0600)
52+
require.NoError(t, err)
53+
fs.Create("/workspace/.devcontainer/devcontainer.json")
54+
55+
// when
56+
options := DefaultOptions()
57+
options.SetString("WorkspaceFolder", "/workspace")
58+
devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&options, &Dependencies{Filesystem: fs})
59+
60+
// then
61+
require.NoError(t, err)
62+
assert.Equal(t, "/workspace/.devcontainer/devcontainer.json", devcontainerPath)
63+
assert.Equal(t, "/workspace/.devcontainer", devcontainerDir)
64+
})
65+
66+
t.Run("overridden .devcontainer directory", func(t *testing.T) {
67+
t.Parallel()
68+
69+
// given
70+
fs := memfs.New()
71+
err := fs.MkdirAll("/workspace/experimental-devcontainer", 0600)
72+
require.NoError(t, err)
73+
fs.Create("/workspace/experimental-devcontainer/devcontainer.json")
74+
75+
// when
76+
options := DefaultOptions()
77+
options.SetString("WorkspaceFolder", "/workspace")
78+
options.SetString("DevcontainerDir", "/experimental-devcontainer")
79+
devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&options, &Dependencies{Filesystem: fs})
80+
81+
// then
82+
require.NoError(t, err)
83+
assert.Equal(t, "/workspace/experimental-devcontainer/devcontainer.json", devcontainerPath)
84+
assert.Equal(t, "/workspace/experimental-devcontainer", devcontainerDir)
85+
})
86+
87+
t.Run("overridden devcontainer.json path", func(t *testing.T) {
88+
t.Parallel()
89+
90+
// given
91+
fs := memfs.New()
92+
err := fs.MkdirAll("/workspace/.devcontainer", 0600)
93+
require.NoError(t, err)
94+
fs.Create("/workspace/.devcontainer/experimental.json")
95+
96+
// when
97+
options := DefaultOptions()
98+
options.SetString("WorkspaceFolder", "/workspace")
99+
options.SetString("DevcontainerJSONPath", "experimental.json")
100+
devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&options, &Dependencies{Filesystem: fs})
101+
102+
// then
103+
require.NoError(t, err)
104+
assert.Equal(t, "/workspace/.devcontainer/experimental.json", devcontainerPath)
105+
assert.Equal(t, "/workspace/.devcontainer", devcontainerDir)
106+
})
107+
108+
t.Run("devcontainer.json in workspace root", func(t *testing.T) {
109+
t.Parallel()
110+
111+
// given
112+
fs := memfs.New()
113+
err := fs.MkdirAll("/workspace", 0600)
114+
require.NoError(t, err)
115+
fs.Create("/workspace/devcontainer.json")
116+
117+
// when
118+
options := DefaultOptions()
119+
options.SetString("WorkspaceFolder", "/workspace")
120+
devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&options, &Dependencies{Filesystem: fs})
121+
122+
// then
123+
require.NoError(t, err)
124+
assert.Equal(t, "/workspace/devcontainer.json", devcontainerPath)
125+
assert.Equal(t, "/workspace", devcontainerDir)
126+
})
127+
128+
t.Run("devcontainer.json in subfolder of .devcontainer", func(t *testing.T) {
129+
t.Parallel()
130+
131+
// given
132+
fs := memfs.New()
133+
err := fs.MkdirAll("/workspace/.devcontainer/sample", 0600)
134+
require.NoError(t, err)
135+
fs.Create("/workspace/.devcontainer/sample/devcontainer.json")
136+
137+
// when
138+
options := DefaultOptions()
139+
options.SetString("WorkspaceFolder", "/workspace")
140+
devcontainerPath, devcontainerDir, err := findDevcontainerJSON(&options, &Dependencies{Filesystem: fs})
141+
142+
// then
143+
require.NoError(t, err)
144+
assert.Equal(t, "/workspace/.devcontainer/sample/devcontainer.json", devcontainerPath)
145+
assert.Equal(t, "/workspace/.devcontainer/sample", devcontainerDir)
146+
})
147+
}

0 commit comments

Comments
 (0)