Skip to content

Commit c8b451f

Browse files
committed
feat: allow changing default workspaces folder
Fixes #384
1 parent c16ae9f commit c8b451f

File tree

6 files changed

+143
-70
lines changed

6 files changed

+143
-70
lines changed

docs/env-variables.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
| `--git-ssh-private-key-path` | `ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH` | | Path to an SSH private key to be used for Git authentication. If this is set, then GIT_SSH_PRIVATE_KEY_BASE64 cannot be set. |
3232
| `--git-ssh-private-key-base64` | `ENVBUILDER_GIT_SSH_PRIVATE_KEY_BASE64` | | Base64 encoded SSH private key to be used for Git authentication. If this is set, then GIT_SSH_PRIVATE_KEY_PATH cannot be set. |
3333
| `--git-http-proxy-url` | `ENVBUILDER_GIT_HTTP_PROXY_URL` | | The URL for the HTTP proxy. This is optional. |
34-
| `--workspace-folder` | `ENVBUILDER_WORKSPACE_FOLDER` | | The path to the workspace folder that will be built. This is optional. |
34+
| `--workspace-folder` | `ENVBUILDER_WORKSPACE_FOLDER` | | The path to the workspace folder that will be built. This is optional. Defaults to `[workspaces folder]/[name]` where name is the name of the repository or `empty`. |
35+
| `--workspaces-folder` | `ENVBUILDER_WORKSPACES_FOLDER` | `/workspaces` | The path under which workspaces will be placed when workspace folder option is not given. |
3536
| `--ssl-cert-base64` | `ENVBUILDER_SSL_CERT_BASE64` | | The content of an SSL cert file. This is useful for self-signed certificates. |
3637
| `--export-env-file` | `ENVBUILDER_EXPORT_ENV_FILE` | | Optional file path to a .env file where envbuilder will dump environment variables from devcontainer.json and the built container image. |
3738
| `--post-start-script-path` | `ENVBUILDER_POST_START_SCRIPT_PATH` | | The path to a script that will be created by envbuilder based on the postStartCommand in devcontainer.json, if any is specified (otherwise the script is not created). If this is set, the specified InitCommand should check for the presence of this script and execute it after successful startup. |

integration/integration_test.go

+22
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,28 @@ func TestBuildFromDevcontainerInCustomPath(t *testing.T) {
834834
require.Equal(t, "hello", strings.TrimSpace(output))
835835
}
836836

837+
func TestBuildFromCustomWorkspacesFolder(t *testing.T) {
838+
t.Parallel()
839+
840+
// Ensures that a Git repository with a devcontainer.json is cloned and built.
841+
srv := gittest.CreateGitServer(t, gittest.Options{
842+
Files: map[string]string{
843+
"Dockerfile": "FROM " + testImageUbuntu,
844+
},
845+
})
846+
ctr, err := runEnvbuilder(t, runOpts{
847+
env: []string{
848+
envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
849+
envbuilderEnv("WORKSPACES_FOLDER", "/foo"),
850+
envbuilderEnv("GIT_URL", srv.URL),
851+
},
852+
})
853+
require.NoError(t, err)
854+
855+
output := execContainer(t, ctr, "readlink /proc/1/cwd")
856+
require.Contains(t, output, "/foo/")
857+
}
858+
837859
func TestBuildFromDevcontainerInSubfolder(t *testing.T) {
838860
t.Parallel()
839861

options/defaults.go

+13-10
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,29 @@ import (
1212
"github.com/coder/envbuilder/internal/workingdir"
1313
)
1414

15-
// EmptyWorkspaceDir is the path to a workspace that has
16-
// nothing going on... it's empty!
17-
var EmptyWorkspaceDir = "/workspaces/empty"
18-
1915
// DefaultWorkspaceFolder returns the default workspace folder
2016
// for a given repository URL.
21-
func DefaultWorkspaceFolder(repoURL string) string {
17+
func DefaultWorkspaceFolder(workspacesFolder, repoURL string) string {
18+
// emptyWorkspaceDir is the path to a workspace that has
19+
// nothing going on... it's empty!
20+
emptyWorkspaceDir := workspacesFolder + "/empty"
21+
2222
if repoURL == "" {
23-
return EmptyWorkspaceDir
23+
return emptyWorkspaceDir
2424
}
2525
parsed, err := giturls.Parse(repoURL)
2626
if err != nil {
27-
return EmptyWorkspaceDir
27+
return emptyWorkspaceDir
2828
}
2929
repo := path.Base(parsed.Path)
3030
// Giturls parsing never actually fails since ParseLocal never
3131
// errors and places the entire URL in the Path field. This check
3232
// ensures it's at least a Unix path containing forwardslash.
3333
if repo == repoURL || repo == "/" || repo == "." || repo == "" {
34-
return EmptyWorkspaceDir
34+
return emptyWorkspaceDir
3535
}
3636
repo = strings.TrimSuffix(repo, ".git")
37-
return fmt.Sprintf("/workspaces/%s", repo)
37+
return fmt.Sprintf("%s/%s", workspacesFolder, repo)
3838
}
3939

4040
func (o *Options) SetDefaults() {
@@ -59,8 +59,11 @@ func (o *Options) SetDefaults() {
5959
if o.Filesystem == nil {
6060
o.Filesystem = chmodfs.New(osfs.New("/"))
6161
}
62+
if o.WorkspacesFolder == "" {
63+
o.WorkspacesFolder = "/workspaces"
64+
}
6265
if o.WorkspaceFolder == "" {
63-
o.WorkspaceFolder = DefaultWorkspaceFolder(o.GitURL)
66+
o.WorkspaceFolder = DefaultWorkspaceFolder(o.WorkspacesFolder, o.GitURL)
6467
}
6568
if o.BinaryPath == "" {
6669
o.BinaryPath = "/.envbuilder/bin/envbuilder"

options/defaults_test.go

+84-56
Original file line numberDiff line numberDiff line change
@@ -16,84 +16,111 @@ func TestDefaultWorkspaceFolder(t *testing.T) {
1616
t.Parallel()
1717

1818
successTests := []struct {
19-
name string
20-
gitURL string
21-
expected string
19+
name string
20+
workspacesFolder string
21+
gitURL string
22+
expected string
2223
}{
2324
{
24-
name: "HTTP",
25-
gitURL: "https://github.com/coder/envbuilder.git",
26-
expected: "/workspaces/envbuilder",
25+
name: "HTTP",
26+
workspacesFolder: "/workspaces",
27+
gitURL: "https://github.com/coder/envbuilder.git",
28+
expected: "/workspaces/envbuilder",
2729
},
2830
{
29-
name: "SSH",
30-
gitURL: "[email protected]:coder/envbuilder.git",
31-
expected: "/workspaces/envbuilder",
31+
name: "SSH",
32+
workspacesFolder: "/workspaces",
33+
gitURL: "[email protected]:coder/envbuilder.git",
34+
expected: "/workspaces/envbuilder",
3235
},
3336
{
34-
name: "username and password",
35-
gitURL: "https://username:[email protected]/coder/envbuilder.git",
36-
expected: "/workspaces/envbuilder",
37+
name: "username and password",
38+
workspacesFolder: "/workspaces",
39+
gitURL: "https://username:[email protected]/coder/envbuilder.git",
40+
expected: "/workspaces/envbuilder",
3741
},
3842
{
39-
name: "trailing",
40-
gitURL: "https://github.com/coder/envbuilder.git/",
41-
expected: "/workspaces/envbuilder",
43+
name: "trailing",
44+
workspacesFolder: "/workspaces",
45+
gitURL: "https://github.com/coder/envbuilder.git/",
46+
expected: "/workspaces/envbuilder",
4247
},
4348
{
44-
name: "trailing-x2",
45-
gitURL: "https://github.com/coder/envbuilder.git//",
46-
expected: "/workspaces/envbuilder",
49+
name: "trailing-x2",
50+
workspacesFolder: "/workspaces",
51+
gitURL: "https://github.com/coder/envbuilder.git//",
52+
expected: "/workspaces/envbuilder",
4753
},
4854
{
49-
name: "no .git",
50-
gitURL: "https://github.com/coder/envbuilder",
51-
expected: "/workspaces/envbuilder",
55+
name: "no .git",
56+
workspacesFolder: "/workspaces",
57+
gitURL: "https://github.com/coder/envbuilder",
58+
expected: "/workspaces/envbuilder",
5259
},
5360
{
54-
name: "trailing no .git",
55-
gitURL: "https://github.com/coder/envbuilder/",
56-
expected: "/workspaces/envbuilder",
61+
name: "trailing no .git",
62+
workspacesFolder: "/workspaces",
63+
gitURL: "https://github.com/coder/envbuilder/",
64+
expected: "/workspaces/envbuilder",
5765
},
5866
{
59-
name: "fragment",
60-
gitURL: "https://github.com/coder/envbuilder.git#feature-branch",
61-
expected: "/workspaces/envbuilder",
67+
name: "fragment",
68+
workspacesFolder: "/workspaces",
69+
gitURL: "https://github.com/coder/envbuilder.git#feature-branch",
70+
expected: "/workspaces/envbuilder",
6271
},
6372
{
64-
name: "fragment-trailing",
65-
gitURL: "https://github.com/coder/envbuilder.git/#refs/heads/feature-branch",
66-
expected: "/workspaces/envbuilder",
73+
name: "fragment-trailing",
74+
workspacesFolder: "/workspaces",
75+
gitURL: "https://github.com/coder/envbuilder.git/#refs/heads/feature-branch",
76+
expected: "/workspaces/envbuilder",
6777
},
6878
{
69-
name: "fragment-trailing no .git",
70-
gitURL: "https://github.com/coder/envbuilder/#refs/heads/feature-branch",
71-
expected: "/workspaces/envbuilder",
79+
name: "fragment-trailing no .git",
80+
workspacesFolder: "/workspaces",
81+
gitURL: "https://github.com/coder/envbuilder/#refs/heads/feature-branch",
82+
expected: "/workspaces/envbuilder",
7283
},
7384
{
74-
name: "space",
75-
gitURL: "https://github.com/coder/env%20builder.git",
76-
expected: "/workspaces/env builder",
85+
name: "space",
86+
workspacesFolder: "/workspaces",
87+
gitURL: "https://github.com/coder/env%20builder.git",
88+
expected: "/workspaces/env builder",
7789
},
7890
{
79-
name: "Unix path",
80-
gitURL: "/repo",
81-
expected: "/workspaces/repo",
91+
name: "Unix path",
92+
workspacesFolder: "/workspaces",
93+
gitURL: "/repo",
94+
expected: "/workspaces/repo",
8295
},
8396
{
84-
name: "Unix subpath",
85-
gitURL: "/path/to/repo",
86-
expected: "/workspaces/repo",
97+
name: "Unix subpath",
98+
workspacesFolder: "/workspaces",
99+
gitURL: "/path/to/repo",
100+
expected: "/workspaces/repo",
87101
},
88102
{
89-
name: "empty",
90-
gitURL: "",
91-
expected: options.EmptyWorkspaceDir,
103+
name: "empty",
104+
workspacesFolder: "/workspaces",
105+
gitURL: "",
106+
expected: "/workspaces/empty",
107+
},
108+
{
109+
name: "non default workspaces folder",
110+
workspacesFolder: "/foo",
111+
gitURL: "https://github.com/coder/envbuilder.git",
112+
expected: "/foo/envbuilder",
113+
},
114+
{
115+
name: "non default workspaces folder empty git URL",
116+
workspacesFolder: "/foo",
117+
gitURL: "",
118+
expected: "/foo/empty",
92119
},
93120
}
94121
for _, tt := range successTests {
95122
t.Run(tt.name, func(t *testing.T) {
96-
dir := options.DefaultWorkspaceFolder(tt.gitURL)
123+
dir := options.DefaultWorkspaceFolder(tt.workspacesFolder, tt.gitURL)
97124
require.Equal(t, tt.expected, dir)
98125
})
99126
}
@@ -125,8 +152,8 @@ func TestDefaultWorkspaceFolder(t *testing.T) {
125152
}
126153
for _, tt := range invalidTests {
127154
t.Run(tt.name, func(t *testing.T) {
128-
dir := options.DefaultWorkspaceFolder(tt.invalidURL)
129-
require.Equal(t, options.EmptyWorkspaceDir, dir)
155+
dir := options.DefaultWorkspaceFolder("/workspaces", tt.invalidURL)
156+
require.Equal(t, "/workspaces/empty", dir)
130157
})
131158
}
132159
}
@@ -135,14 +162,15 @@ func TestOptions_SetDefaults(t *testing.T) {
135162
t.Parallel()
136163

137164
expected := options.Options{
138-
InitScript: "sleep infinity",
139-
InitCommand: "/bin/sh",
140-
IgnorePaths: []string{"/var/run", "/product_uuid", "/product_name"},
141-
Filesystem: chmodfs.New(osfs.New("/")),
142-
GitURL: "",
143-
WorkspaceFolder: options.EmptyWorkspaceDir,
144-
WorkingDirBase: "/.envbuilder",
145-
BinaryPath: "/.envbuilder/bin/envbuilder",
165+
InitScript: "sleep infinity",
166+
InitCommand: "/bin/sh",
167+
IgnorePaths: []string{"/var/run", "/product_uuid", "/product_name"},
168+
Filesystem: chmodfs.New(osfs.New("/")),
169+
GitURL: "",
170+
WorkspacesFolder: "/workspaces",
171+
WorkspaceFolder: "/workspaces/empty",
172+
WorkingDirBase: "/.envbuilder",
173+
BinaryPath: "/.envbuilder/bin/envbuilder",
146174
}
147175

148176
var actual options.Options

options/options.go

+16-3
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,12 @@ type Options struct {
117117
// GitHTTPProxyURL is the URL for the HTTP proxy. This is optional.
118118
GitHTTPProxyURL string
119119
// WorkspaceFolder is the path to the workspace folder that will be built.
120-
// This is optional.
120+
// This is optional. Defaults to `[workspaces folder]/[name]` where name is
121+
// the name of the repository or "empty".
121122
WorkspaceFolder string
123+
// WorkspacesFolder is the path under which workspaces will be placed when
124+
// workspace folder option is not given.
125+
WorkspacesFolder string
122126
// SSLCertBase64 is the content of an SSL cert file. This is useful for
123127
// self-signed certificates.
124128
SSLCertBase64 string
@@ -395,8 +399,17 @@ func (o *Options) CLI() serpent.OptionSet {
395399
Flag: "workspace-folder",
396400
Env: WithEnvPrefix("WORKSPACE_FOLDER"),
397401
Value: serpent.StringOf(&o.WorkspaceFolder),
398-
Description: "The path to the workspace folder that will " +
399-
"be built. This is optional.",
402+
Description: "The path to the workspace folder that will be built. " +
403+
"This is optional. Defaults to `[workspaces folder]/[name]` where " +
404+
"name is the name of the repository or `empty`.",
405+
},
406+
{
407+
Flag: "workspaces-folder",
408+
Env: WithEnvPrefix("WORKSPACES_FOLDER"),
409+
Value: serpent.StringOf(&o.WorkspacesFolder),
410+
Default: "/workspaces",
411+
Description: "The path under which workspaces will be placed when " +
412+
"workspace folder option is not given.",
400413
},
401414
{
402415
Flag: "ssl-cert-base64",

options/testdata/options.golden

+6
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,10 @@ OPTIONS:
178178

179179
--workspace-folder string, $ENVBUILDER_WORKSPACE_FOLDER
180180
The path to the workspace folder that will be built. This is optional.
181+
Defaults to `[workspaces folder]/[name]` where name is the name of the
182+
repository or `empty`.
183+
184+
--workspaces-folder string, $ENVBUILDER_WORKSPACES_FOLDER (default: /workspaces)
185+
The path under which workspaces will be placed when workspace folder
186+
option is not given.
181187

0 commit comments

Comments
 (0)