From 9c8e3a2b225e136ab205435b01e50db89a94bd5e Mon Sep 17 00:00:00 2001 From: jester Date: Sun, 14 Dec 2025 18:45:40 +0000 Subject: [PATCH] dev container provisioning code 12-14-25 --- internal/http/agent.go | 2 +- internal/provision/addons/addons.go | 35 +++ .../provision/addons/codeserver/install.go | 42 ++++ .../provision/addons/codeserver/verify.go | 22 ++ internal/provision/addons/markers.go | 24 +++ internal/provision/provision.go | 203 +++++++++--------- internal/state/state.go | 14 +- scripts/addons/codeserver/install.sh | 9 + scripts/devcontainer/go/install.sh | 12 +- scripts/devcontainer/java/install.sh | 12 +- scripts/devcontainer/node/install.sh | 9 +- scripts/devcontainer/python/install.sh | 12 +- scripts/workspace.sh | 22 ++ 13 files changed, 300 insertions(+), 118 deletions(-) create mode 100644 internal/provision/addons/addons.go create mode 100644 internal/provision/addons/codeserver/install.go create mode 100644 internal/provision/addons/codeserver/verify.go create mode 100644 internal/provision/addons/markers.go create mode 100644 scripts/addons/codeserver/install.sh create mode 100644 scripts/workspace.sh diff --git a/internal/http/agent.go b/internal/http/agent.go index 26b0a67..4b81f59 100755 --- a/internal/http/agent.go +++ b/internal/http/agent.go @@ -13,7 +13,7 @@ import ( "zlh-agent/internal/provision" "zlh-agent/internal/provision/devcontainer" - "zlh-agent/internal/provision/devcontainer/goenv" + "zlh-agent/internal/provision/devcontainer/go" "zlh-agent/internal/provision/devcontainer/java" "zlh-agent/internal/provision/devcontainer/node" "zlh-agent/internal/provision/devcontainer/python" diff --git a/internal/provision/addons/addons.go b/internal/provision/addons/addons.go new file mode 100644 index 0000000..8469573 --- /dev/null +++ b/internal/provision/addons/addons.go @@ -0,0 +1,35 @@ +package addons + +import ( + "fmt" + + "zlh-agent/internal/state" + "zlh-agent/internal/provision/addons/codeserver" +) + +/* + Provision installs requested addons. + + IMPORTANT: + - Addons are role-agnostic (dev/game/etc) + - This function ONLY installs + - Verification happens later (ensureProvisioned) +*/ +func Provision(cfg state.Config) error { + + for _, addon := range cfg.Addons { + + switch addon { + + case "codeserver": + if err := codeserver.Install(cfg); err != nil { + return err + } + + default: + return fmt.Errorf("unsupported addon: %s", addon) + } + } + + return nil +} diff --git a/internal/provision/addons/codeserver/install.go b/internal/provision/addons/codeserver/install.go new file mode 100644 index 0000000..0d203db --- /dev/null +++ b/internal/provision/addons/codeserver/install.go @@ -0,0 +1,42 @@ +package codeserver + +import ( + "fmt" + "path/filepath" + + "zlh-agent/internal/provision" + "zlh-agent/internal/provision/addons" + "zlh-agent/internal/state" +) + +/* + Install installs the code-server addon. + + IMPORTANT: + - Addon-only (not devcontainer-specific) + - No assumptions about users or workspaces + - Idempotent via addon marker +*/ +func Install(cfg state.Config) error { + + if addons.IsAddonProvisioned("codeserver") { + return nil + } + + scriptPath := filepath.Join( + provision.ScriptsRoot, + "addons", + "codeserver", + "install.sh", + ) + + if err := provision.RunScript(scriptPath); err != nil { + return fmt.Errorf("codeserver install failed: %w", err) + } + + if err := addons.WriteAddonMarker("codeserver"); err != nil { + return fmt.Errorf("failed to write codeserver marker: %w", err) + } + + return nil +} diff --git a/internal/provision/addons/codeserver/verify.go b/internal/provision/addons/codeserver/verify.go new file mode 100644 index 0000000..da3af85 --- /dev/null +++ b/internal/provision/addons/codeserver/verify.go @@ -0,0 +1,22 @@ +package codeserver + +import ( + "fmt" + "os/exec" + + "zlh-agent/internal/state" +) + +/* + Verify validates that code-server is installed and usable. + + This does NOT start the service. +*/ +func Verify(cfg state.Config) error { + + if _, err := exec.LookPath("code-server"); err != nil { + return fmt.Errorf("code-server binary not found in PATH") + } + + return nil +} diff --git a/internal/provision/addons/markers.go b/internal/provision/addons/markers.go new file mode 100644 index 0000000..e621a2d --- /dev/null +++ b/internal/provision/addons/markers.go @@ -0,0 +1,24 @@ +package addons + +import ( + "os" + "path/filepath" +) + +const ( + addonMarkerDir = "/opt/zlh/.zlh/addons" +) + +func IsAddonProvisioned(name string) bool { + path := filepath.Join(addonMarkerDir, name) + _, err := os.Stat(path) + return err == nil +} + +func WriteAddonMarker(name string) error { + if err := os.MkdirAll(addonMarkerDir, 0755); err != nil { + return err + } + path := filepath.Join(addonMarkerDir, name) + return os.WriteFile(path, []byte("ok"), 0644) +} diff --git a/internal/provision/provision.go b/internal/provision/provision.go index 3166ce0..f407713 100644 --- a/internal/provision/provision.go +++ b/internal/provision/provision.go @@ -5,6 +5,7 @@ import ( "strings" "zlh-agent/internal/state" + "zlh-agent/internal/provision/addons" "zlh-agent/internal/provision/devcontainer" "zlh-agent/internal/provision/minecraft" "zlh-agent/internal/provision/steam" @@ -20,115 +21,123 @@ import ( func ProvisionAll(cfg state.Config) error { /* --------------------------------------------------------- - DEV CONTAINERS + BASE ROLE PROVISIONING --------------------------------------------------------- */ if cfg.ContainerType == "dev" { - return devcontainer.Provision(cfg) - } - // Legacy / default behavior assumes game containers - game := strings.ToLower(cfg.Game) - variant := strings.ToLower(cfg.Variant) - - /* --------------------------------------------------------- - MINECRAFT - --------------------------------------------------------- */ - if game == "minecraft" { - - // 1. Install Java (runtime) - if err := InstallJava(cfg); err != nil { - return fmt.Errorf("java install failed: %w", err) - } - - // 2. Game variant install - switch variant { - - case "vanilla", "paper", "purpur", "fabric", "quilt": - if err := minecraft.InstallMinecraftVanilla(cfg); err != nil { - return fmt.Errorf("minecraft vanilla install failed: %w", err) - } - - case "forge": - if err := minecraft.InstallMinecraftForge(cfg); err != nil { - return fmt.Errorf("forge install failed: %w", err) - } - - case "neoforge": - if err := minecraft.InstallMinecraftNeoForge(cfg); err != nil { - return fmt.Errorf("neoforge install failed: %w", err) - } - - default: - return fmt.Errorf("unsupported minecraft variant: %s", variant) - } - - // 3. Config files generated AFTER variant installer - if err := WriteEula(cfg); err != nil { - return err - } - if err := WriteServerProperties(cfg); err != nil { - return err - } - if err := WriteStartScript(cfg); err != nil { + if err := devcontainer.Provision(cfg); err != nil { return err } - // DO NOT VERIFY HERE. - return nil + } else { + + // Legacy / default behavior assumes game containers + game := strings.ToLower(cfg.Game) + variant := strings.ToLower(cfg.Variant) + + /* --------------------------------------------------------- + MINECRAFT + --------------------------------------------------------- */ + if game == "minecraft" { + + // 1. Install Java (runtime) + if err := InstallJava(cfg); err != nil { + return fmt.Errorf("java install failed: %w", err) + } + + // 2. Game variant install + switch variant { + + case "vanilla", "paper", "purpur", "fabric", "quilt": + if err := minecraft.InstallMinecraftVanilla(cfg); err != nil { + return fmt.Errorf("minecraft vanilla install failed: %w", err) + } + + case "forge": + if err := minecraft.InstallMinecraftForge(cfg); err != nil { + return fmt.Errorf("forge install failed: %w", err) + } + + case "neoforge": + if err := minecraft.InstallMinecraftNeoForge(cfg); err != nil { + return fmt.Errorf("neoforge install failed: %w", err) + } + + default: + return fmt.Errorf("unsupported minecraft variant: %s", variant) + } + + // 3. Config files generated AFTER variant installer + if err := WriteEula(cfg); err != nil { + return err + } + if err := WriteServerProperties(cfg); err != nil { + return err + } + if err := WriteStartScript(cfg); err != nil { + return err + } + + } else if IsSteamGame(game) { + + /* --------------------------------------------------------- + STEAM GAMES + --------------------------------------------------------- */ + + // 1. SteamCMD install + if err := steam.EnsureSteamCMD(); err != nil { + return fmt.Errorf("steamcmd install failed: %w", err) + } + + // 2. Install game-specific content + switch game { + + case "valheim": + if err := steam.InstallValheim(cfg); err != nil { + return err + } + + case "rust": + if err := steam.InstallRust(cfg); err != nil { + return err + } + + case "terraria": + if err := steam.InstallTerraria(cfg); err != nil { + return err + } + + case "projectzomboid": + if err := steam.InstallProjectZomboid(cfg); err != nil { + return err + } + + default: + return fmt.Errorf("unsupported steam game: %s", game) + } + + // 3. Start script + if err := WriteStartScript(cfg); err != nil { + return err + } + + } else { + return fmt.Errorf( + "unsupported container identity (containerType=%q game=%q)", + cfg.ContainerType, + cfg.Game, + ) + } } /* --------------------------------------------------------- - STEAM GAMES + ADDONS (OPTIONAL, ROLE-AGNOSTIC) --------------------------------------------------------- */ - if IsSteamGame(game) { - - // 1. SteamCMD install - if err := steam.EnsureSteamCMD(); err != nil { - return fmt.Errorf("steamcmd install failed: %w", err) - } - - // 2. Install game-specific content - switch game { - - case "valheim": - if err := steam.InstallValheim(cfg); err != nil { - return err - } - - case "rust": - if err := steam.InstallRust(cfg); err != nil { - return err - } - - case "terraria": - if err := steam.InstallTerraria(cfg); err != nil { - return err - } - - case "projectzomboid": - if err := steam.InstallProjectZomboid(cfg); err != nil { - return err - } - - default: - return fmt.Errorf("unsupported steam game: %s", game) - } - - // 3. Start script - if err := WriteStartScript(cfg); err != nil { + if len(cfg.Addons) > 0 { + if err := addons.Provision(cfg); err != nil { return err } - - // DO NOT VERIFY HERE. - return nil } - /* --------------------------------------------------------- - UNKNOWN CONTAINER TYPE - --------------------------------------------------------- */ - return fmt.Errorf( - "unsupported container identity (containerType=%q game=%q)", - cfg.ContainerType, - cfg.Game, - ) + return nil } diff --git a/internal/state/state.go b/internal/state/state.go index c339957..29fcc1b 100755 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -13,7 +13,17 @@ import ( ----------------------------------------------------------------------------*/ type Config struct { - VMID int `json:"vmid"` + VMID int `json:"vmid"` + + // Container identity + ContainerType string `json:"container_type,omitempty"` + + // Dev runtime (only for dev containers) + Runtime string `json:"runtime,omitempty"` + + // OPTIONAL addons (role-agnostic) + Addons []string `json:"addons,omitempty"` + Game string `json:"game"` Variant string `json:"variant"` Version string `json:"version"` @@ -32,6 +42,8 @@ type Config struct { AdminPass string `json:"admin_pass,omitempty"` } + + /* -------------------------------------------------------------------------- AGENT STATE ENUM ----------------------------------------------------------------------------*/ diff --git a/scripts/addons/codeserver/install.sh b/scripts/addons/codeserver/install.sh new file mode 100644 index 0000000..c8e32b2 --- /dev/null +++ b/scripts/addons/codeserver/install.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -e + +echo "[addon:codeserver] installing code-server" + +# Official install script (can be replaced later if you want full control) +curl -fsSL https://code-server.dev/install.sh | sh + +echo "[addon:codeserver] install complete" diff --git a/scripts/devcontainer/go/install.sh b/scripts/devcontainer/go/install.sh index 0c13d02..f43c268 100644 --- a/scripts/devcontainer/go/install.sh +++ b/scripts/devcontainer/go/install.sh @@ -1,10 +1,12 @@ #!/usr/bin/env bash set -e -echo "[devcontainer:] starting install" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../workspace.sh" -# install runtime -# install basic tooling -# ensure binaries land in PATH +echo "[devcontainer:go] installing golang" -echo "[devcontainer:] install complete" +apt update +apt install -y golang + +echo "[devcontainer:go] install complete" diff --git a/scripts/devcontainer/java/install.sh b/scripts/devcontainer/java/install.sh index 0c13d02..13bdbba 100644 --- a/scripts/devcontainer/java/install.sh +++ b/scripts/devcontainer/java/install.sh @@ -1,10 +1,12 @@ #!/usr/bin/env bash set -e -echo "[devcontainer:] starting install" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../workspace.sh" -# install runtime -# install basic tooling -# ensure binaries land in PATH +echo "[devcontainer:java] installing jdk" -echo "[devcontainer:] install complete" +apt update +apt install -y openjdk-21-jdk + +echo "[devcontainer:java] install complete" diff --git a/scripts/devcontainer/node/install.sh b/scripts/devcontainer/node/install.sh index 829991a..6e186a4 100644 --- a/scripts/devcontainer/node/install.sh +++ b/scripts/devcontainer/node/install.sh @@ -1,11 +1,12 @@ #!/usr/bin/env bash set -e -echo "Installing Node.js dev container runtime" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../workspace.sh" + +echo "[devcontainer:node] installing nodejs" -# example (placeholder) apt update apt install -y nodejs npm -echo "Node version:" -node --version +echo "[devcontainer:node] install complete" diff --git a/scripts/devcontainer/python/install.sh b/scripts/devcontainer/python/install.sh index 0c13d02..e02f303 100644 --- a/scripts/devcontainer/python/install.sh +++ b/scripts/devcontainer/python/install.sh @@ -1,10 +1,12 @@ #!/usr/bin/env bash set -e -echo "[devcontainer:] starting install" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../workspace.sh" -# install runtime -# install basic tooling -# ensure binaries land in PATH +echo "[devcontainer:python] installing python" -echo "[devcontainer:] install complete" +apt update +apt install -y python3 python3-pip python3-venv + +echo "[devcontainer:python] install complete" diff --git a/scripts/workspace.sh b/scripts/workspace.sh new file mode 100644 index 0000000..14efe22 --- /dev/null +++ b/scripts/workspace.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -e + +DEV_USER="devuser" +DEV_HOME="/home/${DEV_USER}" +WORKSPACE="${DEV_HOME}/Workspace" + +echo "[workspace] ensuring dev user and workspace" + +# Create user if missing +if ! id "${DEV_USER}" &>/dev/null; then + useradd -m -s /bin/bash "${DEV_USER}" + echo "[workspace] created user ${DEV_USER}" +fi + +# Ensure workspace exists +mkdir -p "${WORKSPACE}" + +# Ensure ownership +chown -R "${DEV_USER}:${DEV_USER}" "${DEV_HOME}" + +echo "[workspace] ready (${DEV_USER}:${WORKSPACE})"