Skip to content

Commit 24ef801

Browse files
authored
feat: allow changing default workspaces folder (#406)
Fixes #384
1 parent 74695f9 commit 24ef801

File tree

6 files changed

+99
-26
lines changed

6 files changed

+99
-26
lines changed

docs/env-variables.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
| `--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. |
3333
| `--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. |
3434
| `--git-http-proxy-url` | `ENVBUILDER_GIT_HTTP_PROXY_URL` | | The URL for the HTTP proxy. This is optional. |
35-
| `--workspace-folder` | `ENVBUILDER_WORKSPACE_FOLDER` | | The path to the workspace folder that will be built. This is optional. |
35+
| `--workspace-base-dir` | `ENVBUILDER_WORKSPACE_BASE_DIR` | `/workspaces` | The path under which workspaces will be placed when workspace folder option is not given. |
36+
| `--workspace-folder` | `ENVBUILDER_WORKSPACE_FOLDER` | | The path to the workspace folder that will be built. This is optional. Defaults to `[workspace base dir]/[name]` where name is the name of the repository or `empty`. |
3637
| `--ssl-cert-base64` | `ENVBUILDER_SSL_CERT_BASE64` | | The content of an SSL cert file. This is useful for self-signed certificates. |
3738
| `--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. |
3839
| `--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 TestBuildFromCustomWorkspaceBaseDir(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("WORKSPACE_BASE_DIR", "/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.WorkspaceBaseDir == "" {
63+
o.WorkspaceBaseDir = "/workspaces"
64+
}
6265
if o.WorkspaceFolder == "" {
63-
o.WorkspaceFolder = DefaultWorkspaceFolder(o.GitURL)
66+
o.WorkspaceFolder = DefaultWorkspaceFolder(o.WorkspaceBaseDir, o.GitURL)
6467
}
6568
if o.BinaryPath == "" {
6669
o.BinaryPath = "/.envbuilder/bin/envbuilder"

options/defaults_test.go

+40-12
Original file line numberDiff line numberDiff line change
@@ -17,83 +17,110 @@ func TestDefaultWorkspaceFolder(t *testing.T) {
1717

1818
successTests := []struct {
1919
name string
20+
baseDir string
2021
gitURL string
2122
expected string
2223
}{
2324
{
2425
name: "HTTP",
26+
baseDir: "/workspaces",
2527
gitURL: "https://github.com/coder/envbuilder.git",
2628
expected: "/workspaces/envbuilder",
2729
},
2830
{
2931
name: "SSH",
32+
baseDir: "/workspaces",
3033
gitURL: "[email protected]:coder/envbuilder.git",
3134
expected: "/workspaces/envbuilder",
3235
},
3336
{
3437
name: "username and password",
38+
baseDir: "/workspaces",
3539
gitURL: "https://username:[email protected]/coder/envbuilder.git",
3640
expected: "/workspaces/envbuilder",
3741
},
3842
{
3943
name: "trailing",
44+
baseDir: "/workspaces",
4045
gitURL: "https://github.com/coder/envbuilder.git/",
4146
expected: "/workspaces/envbuilder",
4247
},
4348
{
4449
name: "trailing-x2",
50+
baseDir: "/workspaces",
4551
gitURL: "https://github.com/coder/envbuilder.git//",
4652
expected: "/workspaces/envbuilder",
4753
},
4854
{
4955
name: "no .git",
56+
baseDir: "/workspaces",
5057
gitURL: "https://github.com/coder/envbuilder",
5158
expected: "/workspaces/envbuilder",
5259
},
5360
{
5461
name: "trailing no .git",
62+
baseDir: "/workspaces",
5563
gitURL: "https://github.com/coder/envbuilder/",
5664
expected: "/workspaces/envbuilder",
5765
},
5866
{
5967
name: "fragment",
68+
baseDir: "/workspaces",
6069
gitURL: "https://github.com/coder/envbuilder.git#feature-branch",
6170
expected: "/workspaces/envbuilder",
6271
},
6372
{
6473
name: "fragment-trailing",
74+
baseDir: "/workspaces",
6575
gitURL: "https://github.com/coder/envbuilder.git/#refs/heads/feature-branch",
6676
expected: "/workspaces/envbuilder",
6777
},
6878
{
6979
name: "fragment-trailing no .git",
80+
baseDir: "/workspaces",
7081
gitURL: "https://github.com/coder/envbuilder/#refs/heads/feature-branch",
7182
expected: "/workspaces/envbuilder",
7283
},
7384
{
7485
name: "space",
86+
baseDir: "/workspaces",
7587
gitURL: "https://github.com/coder/env%20builder.git",
7688
expected: "/workspaces/env builder",
7789
},
7890
{
7991
name: "Unix path",
92+
baseDir: "/workspaces",
8093
gitURL: "/repo",
8194
expected: "/workspaces/repo",
8295
},
8396
{
8497
name: "Unix subpath",
98+
baseDir: "/workspaces",
8599
gitURL: "/path/to/repo",
86100
expected: "/workspaces/repo",
87101
},
88102
{
89103
name: "empty",
104+
baseDir: "/workspaces",
90105
gitURL: "",
91-
expected: options.EmptyWorkspaceDir,
106+
expected: "/workspaces/empty",
107+
},
108+
{
109+
name: "non default workspaces folder",
110+
baseDir: "/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+
baseDir: "/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.baseDir, 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+
WorkspaceBaseDir: "/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
@@ -120,8 +120,12 @@ type Options struct {
120120
GitSSHPrivateKeyBase64 string
121121
// GitHTTPProxyURL is the URL for the HTTP proxy. This is optional.
122122
GitHTTPProxyURL string
123+
// WorkspaceBaseDir is the path under which workspaces will be placed when
124+
// workspace folder option is not given.
125+
WorkspaceBaseDir string
123126
// WorkspaceFolder is the path to the workspace folder that will be built.
124-
// This is optional.
127+
// This is optional. Defaults to `[workspace base dir]/[name]` where name is
128+
// the name of the repository or "empty".
125129
WorkspaceFolder string
126130
// SSLCertBase64 is the content of an SSL cert file. This is useful for
127131
// self-signed certificates.
@@ -403,12 +407,21 @@ func (o *Options) CLI() serpent.OptionSet {
403407
Value: serpent.StringOf(&o.GitHTTPProxyURL),
404408
Description: "The URL for the HTTP proxy. This is optional.",
405409
},
410+
{
411+
Flag: "workspace-base-dir",
412+
Env: WithEnvPrefix("WORKSPACE_BASE_DIR"),
413+
Value: serpent.StringOf(&o.WorkspaceBaseDir),
414+
Default: "/workspaces",
415+
Description: "The path under which workspaces will be placed when " +
416+
"workspace folder option is not given.",
417+
},
406418
{
407419
Flag: "workspace-folder",
408420
Env: WithEnvPrefix("WORKSPACE_FOLDER"),
409421
Value: serpent.StringOf(&o.WorkspaceFolder),
410-
Description: "The path to the workspace folder that will " +
411-
"be built. This is optional.",
422+
Description: "The path to the workspace folder that will be built. " +
423+
"This is optional. Defaults to `[workspace base dir]/[name]` where " +
424+
"name is the name of the repository or `empty`.",
412425
},
413426
{
414427
Flag: "ssl-cert-base64",

options/testdata/options.golden

+6
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ OPTIONS:
181181
--verbose bool, $ENVBUILDER_VERBOSE
182182
Enable verbose logging.
183183

184+
--workspace-base-dir string, $ENVBUILDER_WORKSPACE_BASE_DIR (default: /workspaces)
185+
The path under which workspaces will be placed when workspace folder
186+
option is not given.
187+
184188
--workspace-folder string, $ENVBUILDER_WORKSPACE_FOLDER
185189
The path to the workspace folder that will be built. This is optional.
190+
Defaults to `[workspace base dir]/[name]` where name is the name of
191+
the repository or `empty`.
186192

0 commit comments

Comments
 (0)