coder.go

128 lines
1 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()
}