package main import ( "bytes" "encoding/json" "errors" "fmt" "os" "os/exec" "regexp" ) // Every server the builder starts, first boots included, is deployed by the node-agent's own code // (BootTest), the same code that runs servers on nodes and in CI, so what the builder sees is what a // node does. This file runs it and keeps the Steam download cache that makes repeated boots cheap. func agentImageFromEnv() string { if img := os.Getenv("AGENT_IMAGE"); img != "" { return img } return defaultAgentImage } // pullAgentImage fetches the latest node-agent image. A copy that is only local (a test build) is used as // it is. func pullAgentImage(img string) error { out, err := runCmd("docker", "pull", "-q", img) if err == nil { return nil } if _, inspectErr := runCmd("docker", "image", "inspect", img); inspectErr == nil { fmt.Printf("[agent] could not pull %s (%s); using the local copy\n", img, firstLine(out)) return nil } return fmt.Errorf("docker pull %s: %v: %s", img, err, out) } // runAgentBootTest deploys a payload with BootTest and returns its output; passed is its verdict. func runAgentBootTest(payload map[string]any, agentImage string) (output string, passed bool, err error) { container := fmt.Sprint(payload["containerName"]) // A run that was killed (worker restarted mid-build) leaves its container and volume behind, and the // next deploy under the same name would trip over them runCmd("docker", "rm", "-f", "-v", container) runCmd("docker", "volume", "rm", "-f", container+"-data") if payload["installMethod"] == "STEAMCMD" { if ic, ok := payload["installConfig"].(map[string]any); ok && ic["appId"] != nil { seedSteamApp(fmt.Sprint(ic["appId"]), container+"-data") } } raw, err := json.Marshal(payload) if err != nil { return "", false, err } cmd := exec.Command("docker", "run", "--rm", "-i", "-v", "/var/run/docker.sock:/var/run/docker.sock", "--entrypoint", "java", agentImage, "-Dloader.main=com.gbreed.gbr.nodeagent.BootTest", "-cp", "app.jar", "org.springframework.boot.loader.launch.PropertiesLauncher") cmd.Stdin = bytes.NewReader(raw) var out bytes.Buffer cmd.Stdout = &teeWriter{&out} cmd.Stderr = cmd.Stdout if runErr := cmd.Run(); runErr != nil { var exitErr *exec.ExitError if !errors.As(runErr, &exitErr) { return "", false, runErr } return out.String(), false, nil } return out.String(), true, nil } // Steam apps whose cached install is up to date in this run (one build or first boot) var steamCacheFresh = map[string]bool{} var numericAppID = regexp.MustCompile(`^\d+$`) // seedSteamApp fills a server volume from a cached SteamCMD install of the app, so a boot doesn't // download the whole game again: the node-agent's own install then only checks the files. The cache is // updated once per run. Without it (STEAM_CACHE=off, or the update failing) the agent downloads as usual. func seedSteamApp(appID, volume string) { if os.Getenv("STEAM_CACHE") == "off" || !numericAppID.MatchString(appID) { return } cache := "gbr-steamcache-" + appID if !steamCacheFresh[appID] { fmt.Printf("[steam-cache] updating the cached install of Steam app %s\n", appID) if out, err := runCmd("docker", "volume", "create", "--label", "gbr.cache=steam", cache); err != nil { fmt.Printf("[steam-cache] %v: %s\n", err, out) return } runCmd("docker", "run", "--rm", "-v", cache+":/server", "alpine", "chown", "-R", nodeUser, "/server") // SteamCMD's first install on a fresh client sometimes fails ("Missing configuration") until it has // logged in once; retried a few times script := "s=/home/steam/steamcmd/steamcmd.sh; $s +login anonymous +quit >/dev/null; for i in 1 2 3; do " + "$s +force_install_dir /server +login anonymous +app_update " + appID + " +quit && exit 0; sleep 15; done; exit 1" cmd := exec.Command("docker", "run", "--rm", "--network", bootTestNetwork, "-v", cache+":/server", "cm2network/steamcmd", "bash", "-c", script) cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("[steam-cache] update failed (%v); the boot downloads the app itself\n", err) return } steamCacheFresh[appID] = true } runCmd("docker", "volume", "create", volume) if out, err := runCmd("docker", "run", "--rm", "-v", cache+":/from:ro", "-v", volume+":/to", "alpine", "cp", "-a", "/from/.", "/to/"); err != nil { fmt.Printf("[steam-cache] copying into %s failed (%v: %s); the boot downloads the app itself\n", volume, err, out) return } fmt.Printf("[steam-cache] %s seeded from the cached install of app %s\n", volume, appID) }