Skip to content

Commit 051bfff

Browse files
committed
Improve readme
1 parent c4c23b9 commit 051bfff

File tree

9 files changed

+348
-15
lines changed

9 files changed

+348
-15
lines changed

.github/workflows/ci.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ name: ci
22

33
on:
44
push:
5-
branches:
6-
- main
75

86
pull_request:
97

README.md

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,112 @@
11
# envbuilder
22

3-
Build development environments from repositories in a container on Kubernetes, Docker, or gVisor. Allow developers to customize their environment on pre-defined infrastructure.
3+
[![discord](https://img.shields.io/discord/747933592273027093?label=discord)](https://discord.gg/coder)
4+
[![release](https://img.shields.io/github/v/release/coder/envbuilder)](https://github.com/coder/envbuilder/releases/latest)
5+
[![godoc](https://pkg.go.dev/badge/github.com/coder/envbuilder.svg)](https://pkg.go.dev/github.com/coder/envbuilder)
6+
[![license](https://img.shields.io/github/license/coder/envbuilder)](./LICENSE)
47

5-
- Supports `devcontainer.json` and `Dockerfile`
8+
Build development environments from a Dockerfile on Docker, Kubernetes, and OpenShift. Allow developers to modify their environment in a tight feedback loop.
9+
10+
- Supports [`devcontainer.json`](https://containers.dev/) and `Dockerfile`
611
- Cache image layers with registries for speedy builds
7-
- Runs on Kubernetes, Docker, and OpenShift
12+
- Runs on Kubernetes, Docker, and OpenShift
13+
14+
<div align="center">
15+
<a href="#gh-light-mode-only">
16+
<img src="./scripts/diagram-light.svg">
17+
</a>
18+
<a href="#gh-dark-mode-only">
19+
<img src="./scripts/diagram-dark.svg">
20+
</a>
21+
</div>
822

923
## Quickstart
1024

11-
The easiest way to play with `envbuilder` is to launch a Docker container that builds a sample image.
25+
The easiest way to get started is to run the `envbuilder` Docker container that clones a repository, builds the image from a Dockerfile, and runs the `$INIT_SCRIPT` in the freshly built container.
26+
27+
> `/tmp/envbuilder` is used to persist data between commands for the purpose of this demo. You can change it to any directory you want.
1228
1329
```bash
1430
docker run -it --rm \
15-
-e GIT_URL=https://github.com/vercel/next.js \
31+
-v /tmp/envbuilder:/workspaces \
32+
-e GIT_URL=https://github.com/coder/envbuilder-starter-devcontainer \
33+
-e INIT_SCRIPT=bash \
1634
ghcr.io/coder/envbuilder
1735
```
36+
37+
Edit `.devcontainer/Dockerfile` to add `htop`:
38+
39+
```bash
40+
$ vim .devcontainer/Dockerfile
41+
```
42+
43+
```diff
44+
- RUN apt-get install vim sudo -y
45+
+ RUN apt-get install vim sudo htop -y
46+
```
47+
48+
Exit the container, and re-run the `docker run` command... after the build completes, `htop` should exist in the container! 🥳
49+
50+
## Container Registry Authentication
51+
52+
envbuilder uses Kaniko to build containers. You should [follow their instructions](https://github.com/GoogleContainerTools/kaniko#pushing-to-different-registries) to create an authentication configuration.
53+
54+
After you have a configuration that resembles the following:
55+
56+
```json
57+
{
58+
"auths": {
59+
"https://index.docker.io/v1/": {
60+
"auth": "base64-encoded-username-and-password"
61+
}
62+
}
63+
}
64+
```
65+
66+
`base64` encode the JSON and provide it to envbuilder as the `DOCKER_CONFIG_BASE64` environment variable.
67+
68+
## Git Authentication
69+
70+
`GIT_USERNAME` and `GIT_PASSWORD` are environment variables to provide Git authentication for private repositories.
71+
72+
For access token-based authentication, follow the following schema (if empty, there's no need to provide the field):
73+
74+
| Provider | `GIT_USERNAME` | `GIT_PASSWORD` |
75+
| ------------ | -------------- | -------------- |
76+
| GitHub | [access-token] | |
77+
| GitLab | oauth2 | [access-token] |
78+
| BitBucket | x-token-auth | [access-token] |
79+
| Azure DevOps | [access-token] | |
80+
81+
If using envbuilder inside of [Coder](https://github.com/coder/coder), you can use the `coder_git_auth` Terraform resource to automatically provide this token on workspace creation:
82+
83+
```hcl
84+
resource "coder_git_auth" "github" {
85+
id = "github"
86+
}
87+
88+
resource "docker_container" "dev" {
89+
env = [
90+
GIT_USERNAME = coder_git_auth.github.access_token,
91+
]
92+
}
93+
```
94+
95+
## Layer Caching
96+
97+
Cache layers in a container registry to speed up builds. To enable caching, [authenticate with your registry](#container-registry-authentication) and set the `CACHE_REPO` environment variable.
98+
99+
```bash
100+
CACHE_REPO=ghcr.io/coder/repo-cache
101+
```
102+
103+
Each layer is stored in the registry as a separate image. The image tag is the hash of the layer's contents. The image digest is the hash of the image tag. The image digest is used to pull the layer from the registry.
104+
105+
## devcontainer.json Support
106+
107+
We don't support mounts, features, and many other primitives of `devcontainer.json`. We support the following:
108+
109+
- `image`
110+
- `build`
111+
- `runArgs`
112+
- `workspaceFolder`

envbuilder.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ func Run(ctx context.Context, options Options) error {
153153

154154
logf(codersdk.LogLevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder"))
155155

156+
var cloned bool
156157
if options.GitURL != "" {
157158
endStage := startStage("📦 Cloning %s to %s...",
158159
newColor(color.FgCyan).Sprintf(options.GitURL),
@@ -188,7 +189,8 @@ func Run(ctx context.Context, options Options) error {
188189
options.GitURL = gitURL.String()
189190
}
190191

191-
cloned, err := CloneRepo(ctx, CloneRepoOptions{
192+
var err error
193+
cloned, err = CloneRepo(ctx, CloneRepoOptions{
192194
Path: options.WorkspaceFolder,
193195
Storage: options.Filesystem,
194196
RepoURL: options.GitURL,
@@ -292,9 +294,8 @@ func Run(ctx context.Context, options Options) error {
292294

293295
build := func() (v1.Image, error) {
294296
endStage := startStage("🏗️ Building image...")
295-
defer endStage("🏗️ Built image!")
296297
// At this point we have all the context, we can now build!
297-
return executor.DoBuild(&config.KanikoOptions{
298+
image, err := executor.DoBuild(&config.KanikoOptions{
298299
// Boilerplate!
299300
CustomPlatform: platforms.Format(platforms.Normalize(platforms.DefaultSpec())),
300301
SnapshotMode: "redo",
@@ -319,6 +320,11 @@ func Run(ctx context.Context, options Options) error {
319320
},
320321
SrcContext: buildParams.BuildContext,
321322
})
323+
if err != nil {
324+
return nil, err
325+
}
326+
endStage("🏗️ Built image!")
327+
return image, err
322328
}
323329

324330
if options.DockerConfigBase64 != "" {
@@ -436,6 +442,30 @@ func Run(ctx context.Context, options Options) error {
436442

437443
unsetOptionsEnv()
438444

445+
// We only need to do this if we cloned!
446+
// Git doesn't store file permissions as part of the repository.
447+
if cloned {
448+
endStage := startStage("🔄 Updating the ownership of the workspace...")
449+
// By default, we clone the Git repository into the workspace folder.
450+
// It will have root permissions, because that's the user that built it.
451+
//
452+
// We need to change the ownership of the files to the user that will
453+
// be running the init script.
454+
filepath.Walk(options.WorkspaceFolder, func(path string, info os.FileInfo, err error) error {
455+
if err != nil {
456+
return err
457+
}
458+
return os.Chown(path, uid, gid)
459+
})
460+
endStage("👤 Updated the ownership of the workspace!")
461+
}
462+
463+
// Remove the Docker config secret file!
464+
err = os.Remove(filepath.Join(os.Getenv("DOCKER_CONFIG"), "config.json"))
465+
if err != nil && !os.IsNotExist(err) {
466+
return fmt.Errorf("remove docker config: %w", err)
467+
}
468+
439469
logf(codersdk.LogLevelInfo, "=== Running the init command %q as the %q user...", options.InitScript, user.Username)
440470
cmd := exec.CommandContext(ctx, "/bin/sh", "-c", options.InitScript)
441471
cmd.Env = os.Environ()

git_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,13 @@ func TestCloneRepo(t *testing.T) {
4444
srv := httptest.NewServer(gittest.NewServer(serverFS))
4545

4646
clientFS := memfs.New()
47-
err = envbuilder.CloneRepo(context.Background(), envbuilder.CloneRepoOptions{
47+
cloned, err := envbuilder.CloneRepo(context.Background(), envbuilder.CloneRepoOptions{
4848
Path: "/workspace",
4949
RepoURL: srv.URL,
5050
Storage: clientFS,
5151
})
5252
require.NoError(t, err)
53+
require.True(t, cloned)
5354

5455
file, err := clientFS.OpenFile("/workspace/README.md", os.O_RDONLY, 0644)
5556
require.NoError(t, err)
@@ -63,11 +64,12 @@ func TestCloneRepo(t *testing.T) {
6364
t.Parallel()
6465
clientFS := memfs.New()
6566
gittest.NewRepo(t, clientFS)
66-
err := envbuilder.CloneRepo(context.Background(), envbuilder.CloneRepoOptions{
67+
cloned, err := envbuilder.CloneRepo(context.Background(), envbuilder.CloneRepoOptions{
6768
Path: "/",
6869
RepoURL: "https://example.com",
6970
Storage: clientFS,
7071
})
7172
require.NoError(t, err)
73+
require.False(t, cloned)
7274
})
7375
}

integration/integration_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ func createGitServer(t *testing.T, opts gitServerOptions) string {
322322
// cleanOldEnvbuilders removes any old envbuilder containers.
323323
func cleanOldEnvbuilders() {
324324
ctx := context.Background()
325-
cli, err := client.NewClientWithOpts(client.FromEnv)
325+
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
326326
if err != nil {
327327
panic(err)
328328
}
@@ -348,7 +348,7 @@ func cleanOldEnvbuilders() {
348348
func runEnvbuilder(t *testing.T, env []string) (string, error) {
349349
t.Helper()
350350
ctx := context.Background()
351-
cli, err := client.NewClientWithOpts(client.FromEnv)
351+
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
352352
require.NoError(t, err)
353353
t.Cleanup(func() {
354354
cli.Close()
@@ -392,7 +392,7 @@ func runEnvbuilder(t *testing.T, env []string) (string, error) {
392392
scanner := bufio.NewScanner(logsReader)
393393
for scanner.Scan() {
394394
t.Logf("%q", strings.TrimSpace(scanner.Text()))
395-
if strings.HasPrefix(scanner.Text(), "error: ") {
395+
if strings.HasPrefix(scanner.Text(), "Error: ") {
396396
errChan <- errors.New(scanner.Text())
397397
return
398398
}

0 commit comments

Comments
 (0)