coder.go
128 lines1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package internal
import (
"cmp"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/exec"
"strings"
"sync"
)
const (
coderContainer = "congo-dev-coder"
coderImage = "codercom/code-server:4.111.0"
coderNetwork = "internal"
)
var (
dataDir = cmp.Or(os.Getenv("DATA_DIR"), "/mnt/data")
// hostDataDir is the host-side path to DATA_DIR. Docker volumes are resolved
// on the host, not inside this container, so we need the host path for mounts.
hostDataDir = cmp.Or(os.Getenv("HOST_DATA_DIR"), dataDir)
proxyOnce sync.Once
proxyHandler http.Handler
)
// EnsureCoder starts the code-server container if it isn't already running.
func EnsureCoder() error {
// Create network (idempotent)
exec.Command("docker", "network", "create", coderNetwork).Run()
if IsCoderRunning() {
log.Println("coder: already running")
connectSelf()
configureGit()
return nil
}
// Prepare workspace directories
workspaceDir := dataDir + "/workspace"
for _, dir := range []string{workspaceDir + "/repos", workspaceDir + "/.config"} {
os.MkdirAll(dir, 0755)
}
exec.Command("chown", "-R", "1000:1000", workspaceDir).Run()
// Remove stale container
exec.Command("docker", "rm", "-f", coderContainer).Run()
// Use host paths for Docker volume mounts (resolved on host, not in this container)
hostWorkspace := hostDataDir + "/workspace"
args := []string{
"run", "-d",
"--name", coderContainer,
"--network", coderNetwork,
"--restart", "always",
"-v", hostWorkspace + ":/home/coder",
"-v", hostWorkspace + "/.config:/home/coder/.config",
"-v", hostDataDir + "/repos:/home/coder/repos",
coderImage,
"--auth", "none",
"--bind-addr", "0.0.0.0:8080",
}
log.Printf("coder: starting container: docker %s", strings.Join(args, " "))
cmd := exec.Command("docker", args...)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("coder: start failed: %s: %w", strings.TrimSpace(string(out)), err)
}
log.Printf("coder: container started: %s", strings.TrimSpace(string(out)))
connectSelf()
configureGit()
return nil
}
// configureGit sets up default git identity in the code-server container.
func configureGit() {
CoderExec("git config --global user.email 'dev@congo.gg'")
CoderExec("git config --global user.name 'Congo Dev'")
CoderExec("git config --global init.defaultBranch main")
}
// connectSelf adds our container to the coder network.
func connectSelf() {
hostname, _ := os.Hostname()
if hostname == "" {
return
}
exec.Command("docker", "network", "connect", coderNetwork, hostname).Run()
}
// IsCoderRunning checks if the code-server container is running.
func IsCoderRunning() bool {
return IsContainerRunning(coderContainer)
}
// CoderExec runs a command inside the code-server container.
func CoderExec(command string) (string, error) {
if !IsCoderRunning() {
return "", fmt.Errorf("coder: container not running")
}
cmd := exec.Command("docker", "exec", coderContainer, "/bin/bash", "-c", command)
out, err := cmd.CombinedOutput()
return string(out), err
}
// CoderProxy returns an HTTP reverse proxy to code-server.
func CoderProxy() http.Handler {
proxyOnce.Do(func() {
target, _ := url.Parse("http://" + coderContainer + ":8080")
proxyHandler = httputil.NewSingleHostReverseProxy(target)
})
return proxyHandler
}
// CoderRestart stops and starts the code-server container.
func CoderRestart() error {
log.Println("coder: restarting...")
exec.Command("docker", "stop", coderContainer).Run()
exec.Command("docker", "rm", "-f", coderContainer).Run()
return EnsureCoder()
}