From 463baf80e0f3a31642e2396076ebe7670a45ba2c Mon Sep 17 00:00:00 2001 From: jester Date: Sun, 14 Dec 2025 16:07:57 +0000 Subject: [PATCH] devcontainer type and code addition 12-14-25 --- internal/http/agent.go | 78 +++++++++++++++---- internal/provision/devcontainer/common.go | 56 +++++++++++++ .../provision/devcontainer/devcontainer.go | 57 ++++++++++++++ internal/provision/devcontainer/go/install.go | 46 +++++++++++ internal/provision/devcontainer/go/verify.go | 36 +++++++++ .../provision/devcontainer/java/install.go | 47 +++++++++++ .../provision/devcontainer/java/verify.go | 35 +++++++++ .../provision/devcontainer/node/install.go | 49 ++++++++++++ .../provision/devcontainer/node/verify.go | 35 +++++++++ .../provision/devcontainer/pyhton/install.go | 0 .../provision/devcontainer/pyhton/verify.go | 0 .../provision/devcontainer/python/install.go | 46 +++++++++++ .../provision/devcontainer/python/verify.go | 35 +++++++++ internal/provision/provision.go | 23 ++++-- scripts/devcontainer/go/install.sh | 10 +++ scripts/devcontainer/java/install.sh | 10 +++ scripts/devcontainer/node/install.sh | 11 +++ scripts/devcontainer/python/install.sh | 10 +++ 18 files changed, 565 insertions(+), 19 deletions(-) delete mode 100644 internal/provision/devcontainer/pyhton/install.go delete mode 100644 internal/provision/devcontainer/pyhton/verify.go create mode 100644 internal/provision/devcontainer/python/install.go create mode 100644 internal/provision/devcontainer/python/verify.go create mode 100644 scripts/devcontainer/go/install.sh create mode 100644 scripts/devcontainer/java/install.sh create mode 100644 scripts/devcontainer/node/install.sh create mode 100644 scripts/devcontainer/python/install.sh diff --git a/internal/http/agent.go b/internal/http/agent.go index 36c397c..26b0a67 100755 --- a/internal/http/agent.go +++ b/internal/http/agent.go @@ -12,6 +12,11 @@ import ( "time" "zlh-agent/internal/provision" + "zlh-agent/internal/provision/devcontainer" + "zlh-agent/internal/provision/devcontainer/goenv" + "zlh-agent/internal/provision/devcontainer/java" + "zlh-agent/internal/provision/devcontainer/node" + "zlh-agent/internal/provision/devcontainer/python" "zlh-agent/internal/provision/minecraft" "zlh-agent/internal/state" "zlh-agent/internal/system" @@ -37,14 +42,13 @@ func runProvisionPipeline(cfg *state.Config) error { state.SetState(state.StateInstalling) state.SetInstallStep("provision_all") - // Installer (downloads files, patches, configs, etc.) if err := provision.ProvisionAll(*cfg); err != nil { state.SetError(err) state.SetState(state.StateError) return err } - // Extra Minecraft verification + // Extra Minecraft verification ONLY for Minecraft if strings.ToLower(cfg.Game) == "minecraft" { if err := minecraft.VerifyMinecraftInstallWithRepair(*cfg); err != nil { state.SetError(err) @@ -59,9 +63,43 @@ func runProvisionPipeline(cfg *state.Config) error { } /* -------------------------------------------------------------------------- - ensureProvisioned() — idempotent, no goto + ensureProvisioned() — idempotent, unified ----------------------------------------------------------------------------*/ func ensureProvisioned(cfg *state.Config) error { + + /* --------------------------------------------------------- + DEV CONTAINERS (FIRST-CLASS) + --------------------------------------------------------- */ + if cfg.ContainerType == "dev" { + + // If not provisioned yet, install + if !devcontainer.IsProvisioned() { + return runProvisionPipeline(cfg) + } + + // Verify runtime + switch strings.ToLower(cfg.Runtime) { + + case "node": + return node.Verify(*cfg) + + case "python": + return python.Verify(*cfg) + + case "go": + return goenv.Verify(*cfg) + + case "java": + return java.Verify(*cfg) + + default: + return fmt.Errorf("unsupported devcontainer runtime: %s", cfg.Runtime) + } + } + + /* --------------------------------------------------------- + GAME SERVERS (EXISTING LOGIC) + --------------------------------------------------------- */ dir := provision.ServerDir(*cfg) game := strings.ToLower(cfg.Game) variant := strings.ToLower(cfg.Variant) @@ -82,7 +120,7 @@ func ensureProvisioned(cfg *state.Config) error { libraries := filepath.Join(dir, "libraries") if fileExists(runSh) && dirExists(libraries) { - return nil // already provisioned + return nil } return runProvisionPipeline(cfg) @@ -91,7 +129,7 @@ func ensureProvisioned(cfg *state.Config) error { // ---------- VANILLA / PAPER / PURPUR / FABRIC ---------- jar := filepath.Join(dir, "server.jar") if fileExists(jar) { - return nil // already provisioned + return nil } return runProvisionPipeline(cfg) @@ -119,7 +157,6 @@ func handleConfig(w http.ResponseWriter, r *http.Request) { return } - // Run provision + server start asynchronously go func(c state.Config) { log.Println("[agent] async provision+start begin") @@ -128,15 +165,18 @@ func handleConfig(w http.ResponseWriter, r *http.Request) { return } - if err := system.StartServer(&c); err != nil { - log.Println("[agent] start error:", err) - state.SetError(err) - state.SetState(state.StateError) - return + // Dev containers may not start a server process + if c.ContainerType != "dev" { + if err := system.StartServer(&c); err != nil { + log.Println("[agent] start error:", err) + state.SetError(err) + state.SetState(state.StateError) + return + } } log.Println("[agent] async provision+start complete") - }(*&cfg) + }(cfg) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusAccepted) @@ -144,7 +184,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) { } /* -------------------------------------------------------------------------- - /start — manual UI start (does NOT re-provision) + /start — manual UI start ----------------------------------------------------------------------------*/ func handleStart(w http.ResponseWriter, r *http.Request) { cfg, err := state.LoadConfig() @@ -153,6 +193,11 @@ func handleStart(w http.ResponseWriter, r *http.Request) { return } + if cfg.ContainerType == "dev" { + http.Error(w, "dev containers do not support manual start", http.StatusBadRequest) + return + } + if err := system.StartServer(cfg); err != nil { http.Error(w, "start error: "+err.Error(), http.StatusInternalServerError) return @@ -183,6 +228,11 @@ func handleRestart(w http.ResponseWriter, r *http.Request) { return } + if cfg.ContainerType == "dev" { + http.Error(w, "dev containers do not support restart", http.StatusBadRequest) + return + } + _ = system.StopServer() if err := system.StartServer(cfg); err != nil { @@ -236,7 +286,7 @@ func handleSendCommand(w http.ResponseWriter, r *http.Request) { } /* -------------------------------------------------------------------------- - Router + WebSocket + Router ----------------------------------------------------------------------------*/ func NewMux() *http.ServeMux { m := http.NewServeMux() diff --git a/internal/provision/devcontainer/common.go b/internal/provision/devcontainer/common.go index e69de29..1814234 100644 --- a/internal/provision/devcontainer/common.go +++ b/internal/provision/devcontainer/common.go @@ -0,0 +1,56 @@ +package devcontainer + +import ( + "encoding/json" + "os" + "path/filepath" + "time" +) + +/* + Dev container provisioning helpers. + + This file is intentionally small and boring. + It exists to support idempotency and shared paths + across dev container runtimes. +*/ + +const ( + // MarkerDir is where devcontainer state is stored. + MarkerDir = "/opt/zlh/.zlh" + + // ReadyMarker is written after a dev container is fully provisioned. + ReadyMarker = "devcontainer_ready.json" +) + +// ReadyMarkerPath returns the absolute path to the ready marker file. +func ReadyMarkerPath() string { + return filepath.Join(MarkerDir, ReadyMarker) +} + +// IsProvisioned returns true if the dev container has already been installed. +func IsProvisioned() bool { + _, err := os.Stat(ReadyMarkerPath()) + return err == nil +} + +// WriteReadyMarker records successful dev container provisioning. +// This should be called by runtime installers AFTER all install steps succeed. +func WriteReadyMarker(runtime string) error { + + if err := os.MkdirAll(MarkerDir, 0755); err != nil { + return err + } + + data := map[string]any{ + "runtime": runtime, + "ready_at": time.Now().UTC().Format(time.RFC3339), + } + + raw, err := json.MarshalIndent(data, "", " ") + if err != nil { + return err + } + + return os.WriteFile(ReadyMarkerPath(), raw, 0644) +} diff --git a/internal/provision/devcontainer/devcontainer.go b/internal/provision/devcontainer/devcontainer.go index e69de29..a8ae588 100644 --- a/internal/provision/devcontainer/devcontainer.go +++ b/internal/provision/devcontainer/devcontainer.go @@ -0,0 +1,57 @@ +package devcontainer + +import ( + "fmt" + "strings" + + "zlh-agent/internal/state" + + devgo "zlh-agent/internal/provision/devcontainer/go" + "zlh-agent/internal/provision/devcontainer/java" + "zlh-agent/internal/provision/devcontainer/node" + "zlh-agent/internal/provision/devcontainer/python" +) + +/* + Provision — entrypoint for dev container provisioning. + + IMPORTANT: + - This function ONLY performs installation. + - Verification happens in ensureProvisioned(). + - state.Config is treated as immutable desired state. + - Routing is based strictly on cfg.Runtime. +*/ +func Provision(cfg state.Config) error { + + runtime := strings.ToLower(cfg.Runtime) + + switch runtime { + + case "node": + if err := node.Install(cfg); err != nil { + return fmt.Errorf("node devcontainer install failed: %w", err) + } + + case "python": + if err := python.Install(cfg); err != nil { + return fmt.Errorf("python devcontainer install failed: %w", err) + } + + case "go": + if err := devgo.Install(cfg); err != nil { + return fmt.Errorf("go devcontainer install failed: %w", err) + } + + case "java": + if err := java.Install(cfg); err != nil { + return fmt.Errorf("java devcontainer install failed: %w", err) + } + + default: + return fmt.Errorf("unsupported dev container runtime: %s", runtime) + } + + // DO NOT VERIFY HERE. + // Verification happens in ensureProvisioned(). + return nil +} diff --git a/internal/provision/devcontainer/go/install.go b/internal/provision/devcontainer/go/install.go index e69de29..ce00ed9 100644 --- a/internal/provision/devcontainer/go/install.go +++ b/internal/provision/devcontainer/go/install.go @@ -0,0 +1,46 @@ +package goenv + +import ( + "fmt" + "path/filepath" + + "zlh-agent/internal/provision" + "zlh-agent/internal/provision/devcontainer" + "zlh-agent/internal/state" +) + +/* + Install installs the Go dev container runtime. + + Execution model: + - Uses local, versioned install scripts from the agent repo + - Scripts handle downloading and installing Go + + IMPORTANT: + - This function ONLY installs + - No verification here +*/ +func Install(cfg state.Config) error { + + // Idempotency guard + if devcontainer.IsProvisioned() { + return nil + } + + scriptPath := filepath.Join( + provision.ScriptsRoot, + "devcontainer", + "go", + "install.sh", + ) + + if err := provision.RunScript(scriptPath); err != nil { + return fmt.Errorf("go devcontainer install failed: %w", err) + } + + if err := devcontainer.WriteReadyMarker("go"); err != nil { + return fmt.Errorf("failed to write devcontainer ready marker: %w", err) + } + + return nil +} diff --git a/internal/provision/devcontainer/go/verify.go b/internal/provision/devcontainer/go/verify.go index e69de29..2454241 100644 --- a/internal/provision/devcontainer/go/verify.go +++ b/internal/provision/devcontainer/go/verify.go @@ -0,0 +1,36 @@ +package goenv + +import ( + "fmt" + "os/exec" + + "zlh-agent/internal/state" +) + +/* + Verify validates that the Go dev container runtime is usable. + + Responsibilities: + - Ensure `go` binary is present and executable + - Ensure GOPATH / GOMOD usage won’t immediately fail + + IMPORTANT: + - No installation here + - No state mutation + - Safe to call multiple times +*/ +func Verify(cfg state.Config) error { + + // Check go binary + if _, err := exec.LookPath("go"); err != nil { + return fmt.Errorf("go binary not found in PATH") + } + + // Optional sanity check: go env should run + cmd := exec.Command("go", "env") + if err := cmd.Run(); err != nil { + return fmt.Errorf("go env failed to execute") + } + + return nil +} diff --git a/internal/provision/devcontainer/java/install.go b/internal/provision/devcontainer/java/install.go index e69de29..824434d 100644 --- a/internal/provision/devcontainer/java/install.go +++ b/internal/provision/devcontainer/java/install.go @@ -0,0 +1,47 @@ +package java + +import ( + "fmt" + "path/filepath" + + "zlh-agent/internal/provision" + "zlh-agent/internal/provision/devcontainer" + "zlh-agent/internal/state" +) + +/* + Install installs the Java dev container runtime. + + Execution model: + - Uses local, versioned install scripts from the agent repo + - Does NOT assume Minecraft semantics + - Provides a general-purpose Java environment for development + + IMPORTANT: + - This function ONLY installs + - No verification here +*/ +func Install(cfg state.Config) error { + + // Idempotency guard + if devcontainer.IsProvisioned() { + return nil + } + + scriptPath := filepath.Join( + provision.ScriptsRoot, + "devcontainer", + "java", + "install.sh", + ) + + if err := provision.RunScript(scriptPath); err != nil { + return fmt.Errorf("java devcontainer install failed: %w", err) + } + + if err := devcontainer.WriteReadyMarker("java"); err != nil { + return fmt.Errorf("failed to write devcontainer ready marker: %w", err) + } + + return nil +} diff --git a/internal/provision/devcontainer/java/verify.go b/internal/provision/devcontainer/java/verify.go index e69de29..26df915 100644 --- a/internal/provision/devcontainer/java/verify.go +++ b/internal/provision/devcontainer/java/verify.go @@ -0,0 +1,35 @@ +package java + +import ( + "fmt" + "os/exec" + + "zlh-agent/internal/state" +) + +/* + Verify validates that the Java dev container runtime is usable. + + Responsibilities: + - Ensure `java` binary is present and executable + - Ensure `javac` is present (development use case) + + IMPORTANT: + - No installation here + - No state mutation + - Safe to call multiple times +*/ +func Verify(cfg state.Config) error { + + // Check java runtime + if _, err := exec.LookPath("java"); err != nil { + return fmt.Errorf("java binary not found in PATH") + } + + // Check Java compiler (dev requirement) + if _, err := exec.LookPath("javac"); err != nil { + return fmt.Errorf("javac binary not found in PATH") + } + + return nil +} diff --git a/internal/provision/devcontainer/node/install.go b/internal/provision/devcontainer/node/install.go index e69de29..3faebab 100644 --- a/internal/provision/devcontainer/node/install.go +++ b/internal/provision/devcontainer/node/install.go @@ -0,0 +1,49 @@ +package node + +import ( + "fmt" + "path/filepath" + + "zlh-agent/internal/provision" + "zlh-agent/internal/provision/devcontainer" + "zlh-agent/internal/state" +) + +/* + Install installs the Node.js dev container runtime. + + Execution model: + - Uses local, versioned install scripts from the agent repo + - Scripts are responsible for fetching artifacts + - Artifact server is NOT an execution source + + IMPORTANT: + - This function ONLY installs + - No verification here +*/ +func Install(cfg state.Config) error { + + // Idempotency guard + if devcontainer.IsProvisioned() { + return nil + } + + // Local script path (inside agent repo / container) + scriptPath := filepath.Join( + provision.ScriptsRoot, + "devcontainer", + "node", + "install.sh", + ) + + if err := provision.RunScript(scriptPath); err != nil { + return fmt.Errorf("node devcontainer install failed: %w", err) + } + + // Mark devcontainer ready + if err := devcontainer.WriteReadyMarker("node"); err != nil { + return fmt.Errorf("failed to write devcontainer ready marker: %w", err) + } + + return nil +} diff --git a/internal/provision/devcontainer/node/verify.go b/internal/provision/devcontainer/node/verify.go index e69de29..62ecff1 100644 --- a/internal/provision/devcontainer/node/verify.go +++ b/internal/provision/devcontainer/node/verify.go @@ -0,0 +1,35 @@ +package node + +import ( + "fmt" + "os/exec" + + "zlh-agent/internal/state" +) + +/* + Verify validates that the Node.js dev container runtime is usable. + + Responsibilities: + - Ensure `node` is present and executable + - Ensure `npm` is present and executable + + IMPORTANT: + - No installation here + - No state mutation + - Safe to call multiple times +*/ +func Verify(cfg state.Config) error { + + // Check node binary + if _, err := exec.LookPath("node"); err != nil { + return fmt.Errorf("node binary not found in PATH") + } + + // Check npm binary + if _, err := exec.LookPath("npm"); err != nil { + return fmt.Errorf("npm binary not found in PATH") + } + + return nil +} diff --git a/internal/provision/devcontainer/pyhton/install.go b/internal/provision/devcontainer/pyhton/install.go deleted file mode 100644 index e69de29..0000000 diff --git a/internal/provision/devcontainer/pyhton/verify.go b/internal/provision/devcontainer/pyhton/verify.go deleted file mode 100644 index e69de29..0000000 diff --git a/internal/provision/devcontainer/python/install.go b/internal/provision/devcontainer/python/install.go new file mode 100644 index 0000000..7641218 --- /dev/null +++ b/internal/provision/devcontainer/python/install.go @@ -0,0 +1,46 @@ +package python + +import ( + "fmt" + "path/filepath" + + "zlh-agent/internal/provision" + "zlh-agent/internal/provision/devcontainer" + "zlh-agent/internal/state" +) + +/* + Install installs the Python dev container runtime. + + Execution model: + - Uses local, versioned install scripts from the agent repo + - Scripts are responsible for fetching artifacts if needed + + IMPORTANT: + - This function ONLY installs + - No verification here +*/ +func Install(cfg state.Config) error { + + // Idempotency guard + if devcontainer.IsProvisioned() { + return nil + } + + scriptPath := filepath.Join( + provision.ScriptsRoot, + "devcontainer", + "python", + "install.sh", + ) + + if err := provision.RunScript(scriptPath); err != nil { + return fmt.Errorf("python devcontainer install failed: %w", err) + } + + if err := devcontainer.WriteReadyMarker("python"); err != nil { + return fmt.Errorf("failed to write devcontainer ready marker: %w", err) + } + + return nil +} diff --git a/internal/provision/devcontainer/python/verify.go b/internal/provision/devcontainer/python/verify.go new file mode 100644 index 0000000..1a58dfe --- /dev/null +++ b/internal/provision/devcontainer/python/verify.go @@ -0,0 +1,35 @@ +package python + +import ( + "fmt" + "os/exec" + + "zlh-agent/internal/state" +) + +/* + Verify validates that the Python dev container runtime is usable. + + Responsibilities: + - Ensure `python3` is present and executable + - Ensure `pip3` is present and executable + + IMPORTANT: + - No installation here + - No state mutation + - Safe to call multiple times +*/ +func Verify(cfg state.Config) error { + + // Check python3 binary + if _, err := exec.LookPath("python3"); err != nil { + return fmt.Errorf("python3 binary not found in PATH") + } + + // Check pip3 binary + if _, err := exec.LookPath("pip3"); err != nil { + return fmt.Errorf("pip3 binary not found in PATH") + } + + return nil +} diff --git a/internal/provision/provision.go b/internal/provision/provision.go index 70b1a03..3166ce0 100644 --- a/internal/provision/provision.go +++ b/internal/provision/provision.go @@ -5,18 +5,28 @@ import ( "strings" "zlh-agent/internal/state" + "zlh-agent/internal/provision/devcontainer" "zlh-agent/internal/provision/minecraft" "zlh-agent/internal/provision/steam" ) /* - ProvisionAll — unified entrypoint for MC + Steam. + ProvisionAll — unified entrypoint for provisioning. IMPORTANT: - This function ONLY performs installation. - Validation/verification happens in ensureProvisioned(). + - state.Config is treated as immutable desired state. */ func ProvisionAll(cfg state.Config) error { + /* --------------------------------------------------------- + DEV CONTAINERS + --------------------------------------------------------- */ + if cfg.ContainerType == "dev" { + return devcontainer.Provision(cfg) + } + + // Legacy / default behavior assumes game containers game := strings.ToLower(cfg.Game) variant := strings.ToLower(cfg.Variant) @@ -64,7 +74,6 @@ func ProvisionAll(cfg state.Config) error { } // DO NOT VERIFY HERE. - // Verification happens in ensureProvisioned(), AFTER install. return nil } @@ -110,12 +119,16 @@ func ProvisionAll(cfg state.Config) error { return err } - // DO NOT VERIFY HERE (Steam verification TBD later) + // DO NOT VERIFY HERE. return nil } /* --------------------------------------------------------- - UNKNOWN GAME TYPE + UNKNOWN CONTAINER TYPE --------------------------------------------------------- */ - return fmt.Errorf("unsupported game type: %s", game) + return fmt.Errorf( + "unsupported container identity (containerType=%q game=%q)", + cfg.ContainerType, + cfg.Game, + ) } diff --git a/scripts/devcontainer/go/install.sh b/scripts/devcontainer/go/install.sh new file mode 100644 index 0000000..0c13d02 --- /dev/null +++ b/scripts/devcontainer/go/install.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -e + +echo "[devcontainer:] starting install" + +# install runtime +# install basic tooling +# ensure binaries land in PATH + +echo "[devcontainer:] install complete" diff --git a/scripts/devcontainer/java/install.sh b/scripts/devcontainer/java/install.sh new file mode 100644 index 0000000..0c13d02 --- /dev/null +++ b/scripts/devcontainer/java/install.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -e + +echo "[devcontainer:] starting install" + +# install runtime +# install basic tooling +# ensure binaries land in PATH + +echo "[devcontainer:] install complete" diff --git a/scripts/devcontainer/node/install.sh b/scripts/devcontainer/node/install.sh new file mode 100644 index 0000000..829991a --- /dev/null +++ b/scripts/devcontainer/node/install.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e + +echo "Installing Node.js dev container runtime" + +# example (placeholder) +apt update +apt install -y nodejs npm + +echo "Node version:" +node --version diff --git a/scripts/devcontainer/python/install.sh b/scripts/devcontainer/python/install.sh new file mode 100644 index 0000000..0c13d02 --- /dev/null +++ b/scripts/devcontainer/python/install.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -e + +echo "[devcontainer:] starting install" + +# install runtime +# install basic tooling +# ensure binaries land in PATH + +echo "[devcontainer:] install complete"