feat(builder): every boot through the node-agent, every lost file counted, Steam cache
What a builder boot sees is now what a node does: - First boots (untouched, or -as-deployed with exactly a new server's settings) are deployed by the node-agent's BootTest in keep mode; the builder only watches the console and inspects what's left. The Go copy of the deploy (its own docker run, mounts and startup prefix) is gone. It had drifted: it gave the image's VOLUME folders their own volumes, which nodes don't, so saves there looked safe while every start loses them. - Files written outside /server and /data are counted whatever their type (it only counted config-like extensions, so .sav or .db saves went unseen), including the image's VOLUME folders; logs, pid and lock files and system folders are left out. - Boot tests ask the server to stay up holdSec (60 s, test.yaml can set it) after its ready line and to listen on every published container port (needs the node-agent's matching BootTest). - Steam apps are downloaded once per build into a gbr.cache volume and copied into each boot, so later boots only check the files (STEAM_CACHE=off). - RUNNER.md no longer suggests docker system prune, which deletes the gbr-boottest network when nothing is using it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e07025f03d
commit
04967a8a16
10 files changed
+424
-171
No files matched your search
@@ -0,0 +1,95 @@
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
Reference in new issue
Block a user