Skip to content

feat: allow changing default workspaces folder (#406) #424

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/env-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
| `--docker-config-base64` | `ENVBUILDER_DOCKER_CONFIG_BASE64` | | The base64 encoded Docker config file that will be used to pull images from private container registries. When this is set, Docker configuration set via the DOCKER_CONFIG environment variable is ignored. |
| `--fallback-image` | `ENVBUILDER_FALLBACK_IMAGE` | | Specifies an alternative image to use when neither an image is declared in the devcontainer.json file nor a Dockerfile is present. If there's a build failure (from a faulty Dockerfile) or a misconfiguration, this image will be the substitute. Set ExitOnBuildFailure to true to halt the container if the build faces an issue. |
| `--exit-on-build-failure` | `ENVBUILDER_EXIT_ON_BUILD_FAILURE` | | Terminates the container upon a build failure. This is handy when preferring the FALLBACK_IMAGE in cases where no devcontainer.json or image is provided. However, it ensures that the container stops if the build process encounters an error. |
| `--exit-on-push-failure` | `ENVBUILDER_EXIT_ON_PUSH_FAILURE` | | ExitOnPushFailure terminates the container upon a push failure. This is useful if failure to push the built image should abort execution and result in an error. |
| `--force-safe` | `ENVBUILDER_FORCE_SAFE` | | Ignores any filesystem safety checks. This could cause serious harm to your system! This is used in cases where bypass is needed to unblock customers. |
| `--insecure` | `ENVBUILDER_INSECURE` | | Bypass TLS verification when cloning and pulling from container registries. |
| `--ignore-paths` | `ENVBUILDER_IGNORE_PATHS` | | The comma separated list of paths to ignore when building the workspace. |
Expand All @@ -30,7 +31,8 @@
| `--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. |
| `--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. |
| `--git-http-proxy-url` | `ENVBUILDER_GIT_HTTP_PROXY_URL` | | The URL for the HTTP proxy. This is optional. |
| `--workspace-folder` | `ENVBUILDER_WORKSPACE_FOLDER` | | The path to the workspace folder that will be built. This is optional. |
| `--workspace-base-dir` | `ENVBUILDER_WORKSPACE_BASE_DIR` | `/workspaces` | The path under which workspaces will be placed when workspace folder option is not given. |
| `--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`. |
| `--ssl-cert-base64` | `ENVBUILDER_SSL_CERT_BASE64` | | The content of an SSL cert file. This is useful for self-signed certificates. |
| `--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. |
| `--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. |
Expand Down
7 changes: 5 additions & 2 deletions envbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -582,10 +582,13 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro
endStage("🏗️ Built image!")
if opts.PushImage {
endStage = startStage("🏗️ Pushing image...")
if err := executor.DoPush(image, kOpts); err != nil {
if err := executor.DoPush(image, kOpts); err == nil {
endStage("🏗️ Pushed image!")
} else if !opts.ExitOnPushFailure {
endStage("⚠️️ Failed to push image!")
} else {
return nil, xerrors.Errorf("do push: %w", err)
}
endStage("🏗️ Pushed image!")
}

return image, err
Expand Down
74 changes: 70 additions & 4 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,28 @@ func TestBuildFromDevcontainerInCustomPath(t *testing.T) {
require.Equal(t, "hello", strings.TrimSpace(output))
}

func TestBuildFromCustomWorkspaceBaseDir(t *testing.T) {
t.Parallel()

// Ensures that a Git repository with a devcontainer.json is cloned and built.
srv := gittest.CreateGitServer(t, gittest.Options{
Files: map[string]string{
"Dockerfile": "FROM " + testImageUbuntu,
},
})
ctr, err := runEnvbuilder(t, runOpts{
env: []string{
envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
envbuilderEnv("WORKSPACE_BASE_DIR", "/foo"),
envbuilderEnv("GIT_URL", srv.URL),
},
})
require.NoError(t, err)

output := execContainer(t, ctr, "readlink /proc/1/cwd")
require.Contains(t, output, "/foo/")
}

func TestBuildFromDevcontainerInSubfolder(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -1879,9 +1901,10 @@ RUN date --utc > /root/date.txt`, testImageAlpine),
_, err = remote.Image(ref, remoteAuthOpt)
require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push")

// When: we run envbuilder with PUSH_IMAGE set
// When: we run envbuilder with PUSH_IMAGE and EXIT_ON_PUSH_FAILURE set
_, err = runEnvbuilder(t, runOpts{env: append(opts,
envbuilderEnv("PUSH_IMAGE", "1"),
envbuilderEnv("EXIT_ON_PUSH_FAILURE", "1"),
)})
// Then: it should fail with an Unauthorized error
require.ErrorContains(t, err, "401 Unauthorized", "expected unauthorized error using no auth when cache repo requires it")
Expand Down Expand Up @@ -2074,7 +2097,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine),
require.ErrorContains(t, err, "--cache-repo must be set when using --push-image")
})

t.Run("PushErr", func(t *testing.T) {
t.Run("PushErr/ExitOnPushFail", func(t *testing.T) {
t.Parallel()

srv := gittest.CreateGitServer(t, gittest.Options{
Expand Down Expand Up @@ -2104,12 +2127,50 @@ RUN date --utc > /root/date.txt`, testImageAlpine),
envbuilderEnv("GIT_URL", srv.URL),
envbuilderEnv("CACHE_REPO", notRegURL),
envbuilderEnv("PUSH_IMAGE", "1"),
envbuilderEnv("EXIT_ON_PUSH_FAILURE", "1"),
}})

// Then: envbuilder should fail with a descriptive error
require.ErrorContains(t, err, "failed to push to destination")
})

t.Run("PushErr/NoExitOnPushFail", func(t *testing.T) {
t.Parallel()

srv := gittest.CreateGitServer(t, gittest.Options{
Files: map[string]string{
".devcontainer/Dockerfile": fmt.Sprintf(`FROM %s
USER root
ARG WORKDIR=/
WORKDIR $WORKDIR
ENV FOO=bar
RUN echo $FOO > /root/foo.txt
RUN date --utc > /root/date.txt`, testImageAlpine),
".devcontainer/devcontainer.json": `{
"name": "Test",
"build": {
"dockerfile": "Dockerfile"
},
}`,
},
})

// Given: registry is not set up (in this case, not a registry)
notRegSrv := httptest.NewServer(http.NotFoundHandler())
notRegURL := strings.TrimPrefix(notRegSrv.URL, "http://") + "/test"

// When: we run envbuilder with PUSH_IMAGE set
_, err := runEnvbuilder(t, runOpts{env: []string{
envbuilderEnv("GIT_URL", srv.URL),
envbuilderEnv("CACHE_REPO", notRegURL),
envbuilderEnv("PUSH_IMAGE", "1"),
envbuilderEnv("EXIT_ON_PUSH_FAILURE", "0"),
}})

// Then: envbuilder should not fail
require.NoError(t, err)
})

t.Run("CacheAndPushDevcontainerFeatures", func(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -2351,8 +2412,13 @@ func pushImage(t *testing.T, ref name.Reference, remoteOpt remote.Option, env ..
if remoteOpt != nil {
remoteOpts = append(remoteOpts, remoteOpt)
}

_, err := runEnvbuilder(t, runOpts{env: append(env, envbuilderEnv("PUSH_IMAGE", "1"))})
opts := runOpts{
env: append(env,
envbuilderEnv("PUSH_IMAGE", "1"),
envbuilderEnv("EXIT_ON_PUSH_FAILURE", "1"),
),
}
_, err := runEnvbuilder(t, opts)
require.NoError(t, err, "envbuilder push image failed")

img, err := remote.Image(ref, remoteOpts...)
Expand Down
23 changes: 13 additions & 10 deletions options/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,29 @@ import (
"github.com/coder/envbuilder/internal/workingdir"
)

// EmptyWorkspaceDir is the path to a workspace that has
// nothing going on... it's empty!
var EmptyWorkspaceDir = "/workspaces/empty"

// DefaultWorkspaceFolder returns the default workspace folder
// for a given repository URL.
func DefaultWorkspaceFolder(repoURL string) string {
func DefaultWorkspaceFolder(workspacesFolder, repoURL string) string {
// emptyWorkspaceDir is the path to a workspace that has
// nothing going on... it's empty!
emptyWorkspaceDir := workspacesFolder + "/empty"

if repoURL == "" {
return EmptyWorkspaceDir
return emptyWorkspaceDir
}
parsed, err := giturls.Parse(repoURL)
if err != nil {
return EmptyWorkspaceDir
return emptyWorkspaceDir
}
repo := path.Base(parsed.Path)
// Giturls parsing never actually fails since ParseLocal never
// errors and places the entire URL in the Path field. This check
// ensures it's at least a Unix path containing forwardslash.
if repo == repoURL || repo == "/" || repo == "." || repo == "" {
return EmptyWorkspaceDir
return emptyWorkspaceDir
}
repo = strings.TrimSuffix(repo, ".git")
return fmt.Sprintf("/workspaces/%s", repo)
return fmt.Sprintf("%s/%s", workspacesFolder, repo)
}

func (o *Options) SetDefaults() {
Expand All @@ -59,8 +59,11 @@ func (o *Options) SetDefaults() {
if o.Filesystem == nil {
o.Filesystem = chmodfs.New(osfs.New("/"))
}
if o.WorkspaceBaseDir == "" {
o.WorkspaceBaseDir = "/workspaces"
}
if o.WorkspaceFolder == "" {
o.WorkspaceFolder = DefaultWorkspaceFolder(o.GitURL)
o.WorkspaceFolder = DefaultWorkspaceFolder(o.WorkspaceBaseDir, o.GitURL)
}
if o.BinaryPath == "" {
o.BinaryPath = "/.envbuilder/bin/envbuilder"
Expand Down
52 changes: 40 additions & 12 deletions options/defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,83 +17,110 @@ func TestDefaultWorkspaceFolder(t *testing.T) {

successTests := []struct {
name string
baseDir string
gitURL string
expected string
}{
{
name: "HTTP",
baseDir: "/workspaces",
gitURL: "https://github.com/coder/envbuilder.git",
expected: "/workspaces/envbuilder",
},
{
name: "SSH",
baseDir: "/workspaces",
gitURL: "[email protected]:coder/envbuilder.git",
expected: "/workspaces/envbuilder",
},
{
name: "username and password",
baseDir: "/workspaces",
gitURL: "https://username:[email protected]/coder/envbuilder.git",
expected: "/workspaces/envbuilder",
},
{
name: "trailing",
baseDir: "/workspaces",
gitURL: "https://github.com/coder/envbuilder.git/",
expected: "/workspaces/envbuilder",
},
{
name: "trailing-x2",
baseDir: "/workspaces",
gitURL: "https://github.com/coder/envbuilder.git//",
expected: "/workspaces/envbuilder",
},
{
name: "no .git",
baseDir: "/workspaces",
gitURL: "https://github.com/coder/envbuilder",
expected: "/workspaces/envbuilder",
},
{
name: "trailing no .git",
baseDir: "/workspaces",
gitURL: "https://github.com/coder/envbuilder/",
expected: "/workspaces/envbuilder",
},
{
name: "fragment",
baseDir: "/workspaces",
gitURL: "https://github.com/coder/envbuilder.git#feature-branch",
expected: "/workspaces/envbuilder",
},
{
name: "fragment-trailing",
baseDir: "/workspaces",
gitURL: "https://github.com/coder/envbuilder.git/#refs/heads/feature-branch",
expected: "/workspaces/envbuilder",
},
{
name: "fragment-trailing no .git",
baseDir: "/workspaces",
gitURL: "https://github.com/coder/envbuilder/#refs/heads/feature-branch",
expected: "/workspaces/envbuilder",
},
{
name: "space",
baseDir: "/workspaces",
gitURL: "https://github.com/coder/env%20builder.git",
expected: "/workspaces/env builder",
},
{
name: "Unix path",
baseDir: "/workspaces",
gitURL: "/repo",
expected: "/workspaces/repo",
},
{
name: "Unix subpath",
baseDir: "/workspaces",
gitURL: "/path/to/repo",
expected: "/workspaces/repo",
},
{
name: "empty",
baseDir: "/workspaces",
gitURL: "",
expected: options.EmptyWorkspaceDir,
expected: "/workspaces/empty",
},
{
name: "non default workspaces folder",
baseDir: "/foo",
gitURL: "https://github.com/coder/envbuilder.git",
expected: "/foo/envbuilder",
},
{
name: "non default workspaces folder empty git URL",
baseDir: "/foo",
gitURL: "",
expected: "/foo/empty",
},
}
for _, tt := range successTests {
t.Run(tt.name, func(t *testing.T) {
dir := options.DefaultWorkspaceFolder(tt.gitURL)
dir := options.DefaultWorkspaceFolder(tt.baseDir, tt.gitURL)
require.Equal(t, tt.expected, dir)
})
}
Expand Down Expand Up @@ -125,8 +152,8 @@ func TestDefaultWorkspaceFolder(t *testing.T) {
}
for _, tt := range invalidTests {
t.Run(tt.name, func(t *testing.T) {
dir := options.DefaultWorkspaceFolder(tt.invalidURL)
require.Equal(t, options.EmptyWorkspaceDir, dir)
dir := options.DefaultWorkspaceFolder("/workspaces", tt.invalidURL)
require.Equal(t, "/workspaces/empty", dir)
})
}
}
Expand All @@ -135,14 +162,15 @@ func TestOptions_SetDefaults(t *testing.T) {
t.Parallel()

expected := options.Options{
InitScript: "sleep infinity",
InitCommand: "/bin/sh",
IgnorePaths: []string{"/var/run", "/product_uuid", "/product_name"},
Filesystem: chmodfs.New(osfs.New("/")),
GitURL: "",
WorkspaceFolder: options.EmptyWorkspaceDir,
MagicDirBase: "/.envbuilder",
BinaryPath: "/.envbuilder/bin/envbuilder",
InitScript: "sleep infinity",
InitCommand: "/bin/sh",
IgnorePaths: []string{"/var/run", "/product_uuid", "/product_name"},
Filesystem: chmodfs.New(osfs.New("/")),
GitURL: "",
WorkspaceBaseDir: "/workspaces",
WorkspaceFolder: "/workspaces/empty",
MagicDirBase: "/.envbuilder",
BinaryPath: "/.envbuilder/bin/envbuilder",
}

var actual options.Options
Expand Down
Loading