devcontainer type and code addition 12-14-25

This commit is contained in:
jester 2025-12-14 16:07:57 +00:00
parent 5319b594e0
commit 463baf80e0
18 changed files with 565 additions and 19 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -e
echo "[devcontainer:<runtime>] starting install"
# install runtime
# install basic tooling
# ensure binaries land in PATH
echo "[devcontainer:<runtime>] install complete"

View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -e
echo "[devcontainer:<runtime>] starting install"
# install runtime
# install basic tooling
# ensure binaries land in PATH
echo "[devcontainer:<runtime>] install complete"

View File

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

View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -e
echo "[devcontainer:<runtime>] starting install"
# install runtime
# install basic tooling
# ensure binaries land in PATH
echo "[devcontainer:<runtime>] install complete"