Skip to content

Commit c715aaa

Browse files
committed
Initial commit
0 parents  commit c715aaa

18 files changed

+5004
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
scripts/envbuilder

LICENSE

Lines changed: 661 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# envbuilder
2+
3+
Build a development environment from `devcontainer.json` or `Dockerfile` inside of a container. Enable developers to customize their environment on pre-defined infrastructure.
4+
5+
- Supports `devcontainer.json` and `Dockerfile`
6+
- Cache image layers with registries
7+
- Runs in Docker, Kubernetes, or gVisor
8+
9+
## Quickstart
10+
11+
The easiest way to play with `envbuilder` is to launch a Docker container that builds a sample image.
12+
13+
```bash
14+
docker run -it --rm \
15+
-e REPO_URL=https://github.com/vercel/next.js \
16+
ghcr.io/coder/envbuilder
17+
```

cmd/envbuilder/main.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package main
2+
3+
import (
4+
"crypto/tls"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
"net/url"
9+
"os"
10+
"time"
11+
12+
"github.com/coder/coder/codersdk/agentsdk"
13+
"github.com/coder/envbuilder"
14+
"github.com/spf13/cobra"
15+
16+
// *Never* remove this. Certificates are not bundled as part
17+
// of the container, so this is necessary for all connections
18+
// to not be insecure.
19+
_ "github.com/breml/rootcerts"
20+
)
21+
22+
func main() {
23+
root := &cobra.Command{
24+
Use: "envbuilder",
25+
RunE: func(cmd *cobra.Command, args []string) error {
26+
options := envbuilder.SystemOptions(os.Getenv)
27+
28+
var sendLogs func(log agentsdk.StartupLog)
29+
agentURL := os.Getenv("CODER_AGENT_URL")
30+
agentToken := os.Getenv("CODER_AGENT_TOKEN")
31+
if agentToken != "" {
32+
if agentURL == "" {
33+
return errors.New("CODER_AGENT_URL must be set if CODER_AGENT_TOKEN is set")
34+
}
35+
parsed, err := url.Parse(agentURL)
36+
if err != nil {
37+
return err
38+
}
39+
client := agentsdk.New(parsed)
40+
client.SetSessionToken(agentToken)
41+
client.SDK.HTTPClient = &http.Client{
42+
Transport: &http.Transport{
43+
TLSClientConfig: &tls.Config{
44+
InsecureSkipVerify: options.Insecure,
45+
},
46+
},
47+
}
48+
sendLogs, err = envbuilder.SendLogsToCoder(cmd.Context(), client, func(format string, args ...any) {
49+
fmt.Fprintf(cmd.ErrOrStderr(), format, args...)
50+
})
51+
if err != nil {
52+
return err
53+
}
54+
}
55+
56+
options.Logger = func(format string, args ...interface{}) {
57+
output := fmt.Sprintf(format, args...)
58+
fmt.Fprintln(cmd.ErrOrStderr(), output)
59+
if sendLogs != nil {
60+
sendLogs(agentsdk.StartupLog{
61+
CreatedAt: time.Now(),
62+
Output: output,
63+
})
64+
}
65+
}
66+
return envbuilder.Run(cmd.Context(), options)
67+
},
68+
}
69+
err := root.Execute()
70+
if err != nil {
71+
fmt.Fprintf(os.Stderr, "error: %v", err)
72+
os.Exit(1)
73+
}
74+
}

devcontainer.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package envbuilder
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
7+
"github.com/go-git/go-billy/v5"
8+
"muzzammil.xyz/jsonc"
9+
)
10+
11+
// ParseDevcontainer parses a devcontainer.json file.
12+
func ParseDevcontainer(content []byte) (*DevContainer, error) {
13+
content = jsonc.ToJSON(content)
14+
var schema DevContainer
15+
return &schema, jsonc.Unmarshal(content, &schema)
16+
}
17+
18+
type DevContainer struct {
19+
Image string `json:"image"`
20+
Build DevContainerBuild `json:"build"`
21+
RemoteUser string `json:"remoteUser"`
22+
RemoteEnv map[string]string `json:"remoteEnv"`
23+
}
24+
25+
type DevContainerBuild struct {
26+
Dockerfile string `json:"dockerfile"`
27+
Context string `json:"context"`
28+
Args map[string]string `json:"args"`
29+
Target string `json:"target"`
30+
CacheFrom string `json:"cache_from"`
31+
}
32+
33+
// Compile returns the build parameters for the workspace.
34+
// devcontainerDir is the path to the directory where the devcontainer.json file
35+
// is located. scratchDir is the path to the directory where the Dockerfile will
36+
// be written to if one doesn't exist.
37+
func (d *DevContainer) Compile(fs billy.Filesystem, devcontainerDir, scratchDir string) (*BuildParameters, error) {
38+
env := make([]string, 0)
39+
for key, value := range d.RemoteEnv {
40+
env = append(env, key+"="+value)
41+
}
42+
params := &BuildParameters{
43+
User: d.RemoteUser,
44+
Env: env,
45+
}
46+
47+
if d.Image != "" {
48+
// We just write the image to a file and return it.
49+
dockerfilePath := filepath.Join(scratchDir, "Dockerfile")
50+
file, err := fs.OpenFile(dockerfilePath, os.O_CREATE|os.O_WRONLY, 0644)
51+
if err != nil {
52+
return nil, err
53+
}
54+
defer file.Close()
55+
_, err = file.Write([]byte("FROM " + d.Image))
56+
if err != nil {
57+
return nil, err
58+
}
59+
params.DockerfilePath = dockerfilePath
60+
params.BuildContext = scratchDir
61+
return params, nil
62+
}
63+
buildArgs := make([]string, 0)
64+
for key, value := range d.Build.Args {
65+
buildArgs = append(buildArgs, key+"="+value)
66+
}
67+
params.BuildArgs = buildArgs
68+
params.Cache = true
69+
params.DockerfilePath = filepath.Join(devcontainerDir, d.Build.Dockerfile)
70+
params.BuildContext = filepath.Join(devcontainerDir, d.Build.Context)
71+
return params, nil
72+
}

devcontainer_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package envbuilder_test
2+
3+
import (
4+
"path/filepath"
5+
"testing"
6+
7+
"github.com/coder/envbuilder"
8+
"github.com/go-git/go-billy/v5/memfs"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestParseDevContainer(t *testing.T) {
13+
t.Parallel()
14+
raw := `{
15+
"build": {
16+
"dockerfile": "Dockerfile",
17+
"context": "."
18+
},
19+
// Comments here!
20+
"image": "codercom/code-server:latest"
21+
}`
22+
parsed, err := envbuilder.ParseDevcontainer([]byte(raw))
23+
require.NoError(t, err)
24+
require.Equal(t, "Dockerfile", parsed.Build.Dockerfile)
25+
}
26+
27+
func TestCompileDevContainer(t *testing.T) {
28+
t.Parallel()
29+
t.Run("WithImage", func(t *testing.T) {
30+
t.Parallel()
31+
fs := memfs.New()
32+
dc := &envbuilder.DevContainer{
33+
Image: "codercom/code-server:latest",
34+
}
35+
params, err := dc.Compile(fs, "", envbuilder.MagicDir)
36+
require.NoError(t, err)
37+
require.Equal(t, filepath.Join(envbuilder.MagicDir, "Dockerfile"), params.DockerfilePath)
38+
require.Equal(t, envbuilder.MagicDir, params.BuildContext)
39+
})
40+
t.Run("WithBuild", func(t *testing.T) {
41+
t.Parallel()
42+
fs := memfs.New()
43+
dc := &envbuilder.DevContainer{
44+
Build: envbuilder.DevContainerBuild{
45+
Dockerfile: "Dockerfile",
46+
Context: ".",
47+
Args: map[string]string{
48+
"ARG1": "value1",
49+
},
50+
},
51+
}
52+
dcDir := "/workspaces/coder/.devcontainer"
53+
err := fs.MkdirAll(dcDir, 0755)
54+
require.NoError(t, err)
55+
params, err := dc.Compile(fs, dcDir, envbuilder.MagicDir)
56+
require.NoError(t, err)
57+
require.Equal(t, "ARG1=value1", params.BuildArgs[0])
58+
require.Equal(t, filepath.Join(dcDir, "Dockerfile"), params.DockerfilePath)
59+
require.Equal(t, dcDir, params.BuildContext)
60+
})
61+
}

0 commit comments

Comments
 (0)