dev container provisioning code 12-14-25

This commit is contained in:
jester 2025-12-14 18:45:40 +00:00
parent 463baf80e0
commit 9c8e3a2b22
13 changed files with 300 additions and 118 deletions

View File

@ -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"

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
----------------------------------------------------------------------------*/

View File

@ -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"

View File

@ -1,10 +1,12 @@
#!/usr/bin/env bash
set -e
echo "[devcontainer:<runtime>] 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:<runtime>] install complete"
apt update
apt install -y golang
echo "[devcontainer:go] install complete"

View File

@ -1,10 +1,12 @@
#!/usr/bin/env bash
set -e
echo "[devcontainer:<runtime>] 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:<runtime>] install complete"
apt update
apt install -y openjdk-21-jdk
echo "[devcontainer:java] install complete"

View File

@ -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"

View File

@ -1,10 +1,12 @@
#!/usr/bin/env bash
set -e
echo "[devcontainer:<runtime>] 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:<runtime>] install complete"
apt update
apt install -y python3 python3-pip python3-venv
echo "[devcontainer:python] install complete"

22
scripts/workspace.sh Normal file
View File

@ -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})"