devcontainer provivioning update 11-14-25

This commit is contained in:
jester 2025-12-14 22:28:30 +00:00
parent 9c8e3a2b22
commit 46637f6759
20 changed files with 348 additions and 257 deletions

View File

@ -4,39 +4,28 @@ import (
"fmt"
"path/filepath"
"zlh-agent/internal/provision"
"zlh-agent/internal/provision/addons"
"zlh-agent/internal/provision/executil"
"zlh-agent/internal/provision/markers"
"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 {
const marker = "addon-codeserver"
if addons.IsAddonProvisioned("codeserver") {
if markers.IsPresent(marker) {
return nil
}
scriptPath := filepath.Join(
provision.ScriptsRoot,
executil.ScriptsRoot,
"addons",
"codeserver",
"install.sh",
)
if err := provision.RunScript(scriptPath); err != nil {
if err := executil.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
return markers.Write(marker)
}

View File

@ -2,17 +2,17 @@ package codeserver
import (
"fmt"
"os"
"os/exec"
"zlh-agent/internal/state"
)
/*
Verify validates that code-server is installed and usable.
const marker = "/opt/zlh/.zlh/addons/code-server.installed"
This does NOT start the service.
*/
func Verify(cfg state.Config) error {
func Verify() error {
if _, err := os.Stat(marker); err != nil {
return fmt.Errorf("code-server addon marker missing")
}
if _, err := exec.LookPath("code-server"); err != nil {
return fmt.Errorf("code-server binary not found in PATH")

View File

@ -12,46 +12,19 @@ import (
"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)
}
return node.Install(cfg)
case "python":
if err := python.Install(cfg); err != nil {
return fmt.Errorf("python devcontainer install failed: %w", err)
}
return python.Install(cfg)
case "go":
if err := devgo.Install(cfg); err != nil {
return fmt.Errorf("go devcontainer install failed: %w", err)
}
return devgo.Install(cfg)
case "java":
if err := java.Install(cfg); err != nil {
return fmt.Errorf("java devcontainer install failed: %w", err)
}
return java.Install(cfg)
default:
return fmt.Errorf("unsupported dev container runtime: %s", runtime)
}
// DO NOT VERIFY HERE.
// Verification happens in ensureProvisioned().
return nil
}

View File

@ -4,43 +4,28 @@ import (
"fmt"
"path/filepath"
"zlh-agent/internal/provision"
"zlh-agent/internal/provision/devcontainer"
"zlh-agent/internal/provision/executil"
"zlh-agent/internal/provision/markers"
"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 {
const marker = "devcontainer-go"
// Idempotency guard
if devcontainer.IsProvisioned() {
if markers.IsPresent(marker) {
return nil
}
scriptPath := filepath.Join(
provision.ScriptsRoot,
executil.ScriptsRoot,
"devcontainer",
"go",
"install.sh",
)
if err := provision.RunScript(scriptPath); err != nil {
if err := executil.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
return markers.Write(marker)
}

View File

@ -7,30 +7,9 @@ import (
"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

@ -4,44 +4,27 @@ import (
"fmt"
"path/filepath"
"zlh-agent/internal/provision"
"zlh-agent/internal/provision/devcontainer"
"zlh-agent/internal/provision/executil"
"zlh-agent/internal/provision/markers"
"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() {
if markers.IsPresent("devcontainer-java") {
return nil
}
scriptPath := filepath.Join(
provision.ScriptsRoot,
executil.ScriptsRoot,
"devcontainer",
"java",
"install.sh",
)
if err := provision.RunScript(scriptPath); err != nil {
if err := executil.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
return markers.Write("devcontainer-java")
}

View File

@ -7,29 +7,9 @@ import (
"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

@ -4,46 +4,28 @@ import (
"fmt"
"path/filepath"
"zlh-agent/internal/provision"
"zlh-agent/internal/provision/devcontainer"
"zlh-agent/internal/provision/executil"
"zlh-agent/internal/provision/markers"
"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 {
const marker = "devcontainer-node"
// Idempotency guard
if devcontainer.IsProvisioned() {
if markers.IsPresent(marker) {
return nil
}
// Local script path (inside agent repo / container)
scriptPath := filepath.Join(
provision.ScriptsRoot,
executil.ScriptsRoot,
"devcontainer",
"node",
"install.sh",
)
if err := provision.RunScript(scriptPath); err != nil {
if err := executil.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
return markers.Write(marker)
}

View File

@ -7,26 +7,13 @@ import (
"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
// Version is optional at verify-time; existence is authoritative
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")
}

View File

@ -4,43 +4,28 @@ import (
"fmt"
"path/filepath"
"zlh-agent/internal/provision"
"zlh-agent/internal/provision/devcontainer"
"zlh-agent/internal/provision/executil"
"zlh-agent/internal/provision/markers"
"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 {
const marker = "devcontainer-python"
// Idempotency guard
if devcontainer.IsProvisioned() {
if markers.IsPresent(marker) {
return nil
}
scriptPath := filepath.Join(
provision.ScriptsRoot,
executil.ScriptsRoot,
"devcontainer",
"python",
"install.sh",
)
if err := provision.RunScript(scriptPath); err != nil {
if err := executil.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
return markers.Write(marker)
}

View File

@ -7,29 +7,12 @@ import (
"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

@ -0,0 +1,15 @@
package executil
import (
"os"
"os/exec"
)
const ScriptsRoot = "/opt/zlh-agent/scripts"
func RunScript(path string) error {
cmd := exec.Command("/bin/bash", path)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}

View File

@ -0,0 +1,22 @@
package markers
import (
"os"
"path/filepath"
)
const (
baseDir = "/opt/zlh/.zlh"
)
func IsPresent(name string) bool {
_, err := os.Stat(filepath.Join(baseDir, name))
return err == nil
}
func Write(name string) error {
if err := os.MkdirAll(baseDir, 0755); err != nil {
return err
}
return os.WriteFile(filepath.Join(baseDir, name), []byte("ok"), 0644)
}

View File

@ -1,9 +0,0 @@
#!/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

@ -0,0 +1,60 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[code-server] starting install"
# --------------------------------------------------
# Config
# --------------------------------------------------
ADDON_ROOT="/opt/zlh/addons/code-server"
ARTIFACT_DIR="/opt/zlh/addons/code-server"
MARKER="/opt/zlh/.zlh/addons/code-server.installed"
mkdir -p "$(dirname "${MARKER}")"
# --------------------------------------------------
# Idempotency
# --------------------------------------------------
if [ -f "${MARKER}" ]; then
echo "[code-server] already installed"
exit 0
fi
ARCHIVE="$(ls ${ARTIFACT_DIR}/code-server.* 2>/dev/null | head -n1)"
if [ -z "${ARCHIVE}" ]; then
echo "[code-server][ERROR] artifact not found"
exit 1
fi
echo "[code-server] extracting ${ARCHIVE}"
mkdir -p "${ADDON_ROOT}"
TMP_DIR="$(mktemp -d)"
case "${ARCHIVE}" in
*.tar.gz)
tar -xzf "${ARCHIVE}" -C "${TMP_DIR}"
;;
*.zip)
unzip -q "${ARCHIVE}" -d "${TMP_DIR}"
;;
*)
echo "[code-server][ERROR] unsupported archive format"
exit 1
;;
esac
EXTRACTED_DIR="$(find ${TMP_DIR} -maxdepth 1 -type d -name 'code-server*' | head -n1)"
if [ -z "${EXTRACTED_DIR}" ]; then
echo "[code-server][ERROR] failed to locate extracted directory"
exit 1
fi
mv "${EXTRACTED_DIR}"/* "${ADDON_ROOT}/"
rm -rf "${TMP_DIR}"
chmod +x "${ADDON_ROOT}/bin/code-server"
touch "${MARKER}"
echo "[code-server] install complete"

View File

@ -1,12 +1,51 @@
#!/usr/bin/env bash
set -e
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../workspace.sh"
echo "[go] starting go devcontainer install"
echo "[devcontainer:go] installing golang"
# --------------------------------------------------
# Config
# --------------------------------------------------
RUNTIME_ROOT="/opt/zlh/runtime/go"
ARTIFACT_ROOT="/opt/zlh/devcontainer/go"
apt update
apt install -y golang
VERSION="${RUNTIME_VERSION:-1.25}"
echo "[devcontainer:go] install complete"
SRC_DIR="${ARTIFACT_ROOT}/${VERSION}"
DEST_DIR="${RUNTIME_ROOT}/${VERSION}"
SYMLINK="${RUNTIME_ROOT}/current"
# --------------------------------------------------
# Guards
# --------------------------------------------------
if [ ! -d "${SRC_DIR}" ]; then
echo "[go][ERROR] artifact directory not found: ${SRC_DIR}"
exit 1
fi
if [ -x "${DEST_DIR}/bin/go" ]; then
echo "[go] go ${VERSION} already installed"
else
ARCHIVE="$(ls ${SRC_DIR}/go*.linux-amd64.tar.gz 2>/dev/null | head -n1)"
if [ -z "${ARCHIVE}" ]; then
echo "[go][ERROR] no Go archive found in ${SRC_DIR}"
exit 1
fi
echo "[go] installing go ${VERSION}"
mkdir -p "${DEST_DIR}"
tar -xzf "${ARCHIVE}" -C "${DEST_DIR}" --strip-components=1
fi
# --------------------------------------------------
# Symlink current
# --------------------------------------------------
ln -sfn "${DEST_DIR}" "${SYMLINK}"
# --------------------------------------------------
# Verify
# --------------------------------------------------
"${SYMLINK}/bin/go" version
echo "[go] go ${VERSION} install complete"

View File

@ -1,12 +1,62 @@
#!/usr/bin/env bash
set -e
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../workspace.sh"
echo "[java] starting java devcontainer install"
echo "[devcontainer:java] installing jdk"
# --------------------------------------------------
# Config
# --------------------------------------------------
RUNTIME_ROOT="/opt/zlh/runtime/java"
ARTIFACT_ROOT="/opt/zlh/devcontainer/java"
apt update
apt install -y openjdk-21-jdk
VERSION="${RUNTIME_VERSION:-17}"
echo "[devcontainer:java] install complete"
SRC_DIR="${ARTIFACT_ROOT}/${VERSION}"
DEST_DIR="${RUNTIME_ROOT}/${VERSION}"
SYMLINK="${RUNTIME_ROOT}/current"
# --------------------------------------------------
# Guards
# --------------------------------------------------
if [ ! -d "${SRC_DIR}" ]; then
echo "[java][ERROR] artifact directory not found: ${SRC_DIR}"
exit 1
fi
if [ -x "${DEST_DIR}/bin/java" ]; then
echo "[java] java ${VERSION} already installed"
else
ARCHIVE="$(ls ${SRC_DIR}/*.tar.gz 2>/dev/null | head -n1)"
if [ -z "${ARCHIVE}" ]; then
echo "[java][ERROR] no Java archive found in ${SRC_DIR}"
exit 1
fi
echo "[java] installing java ${VERSION}"
mkdir -p "${DEST_DIR}"
TMP_DIR="$(mktemp -d)"
tar -xzf "${ARCHIVE}" -C "${TMP_DIR}"
# Java archives contain a single root dir
EXTRACTED_DIR="$(find ${TMP_DIR} -maxdepth 1 -type d -name 'jdk*' | head -n1)"
if [ -z "${EXTRACTED_DIR}" ]; then
echo "[java][ERROR] failed to locate extracted jdk directory"
exit 1
fi
mv "${EXTRACTED_DIR}"/* "${DEST_DIR}/"
rm -rf "${TMP_DIR}"
fi
# --------------------------------------------------
# Symlink current
# --------------------------------------------------
ln -sfn "${DEST_DIR}" "${SYMLINK}"
# --------------------------------------------------
# Verify
# --------------------------------------------------
"${SYMLINK}/bin/java" -version
echo "[java] java ${VERSION} install complete"

View File

@ -1,12 +1,40 @@
#!/usr/bin/env bash
set -e
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../workspace.sh"
RUNTIME_ROOT="/opt/zlh/runtime/node"
ARTIFACT_ROOT="/opt/zlh/devcontainer/node"
echo "[devcontainer:node] installing nodejs"
VERSION="${RUNTIME_VERSION:-24}"
apt update
apt install -y nodejs npm
ARCHIVE="node-v${VERSION}.*-linux-x64.tar.xz"
SRC_DIR="${ARTIFACT_ROOT}/${VERSION}"
DEST_DIR="${RUNTIME_ROOT}/${VERSION}"
echo "[devcontainer:node] install complete"
echo "[node] Installing Node.js version ${VERSION}"
# Ensure runtime root exists
mkdir -p "${RUNTIME_ROOT}"
# Idempotency check
if [ -d "${DEST_DIR}" ]; then
echo "[node] Node ${VERSION} already installed"
else
ARCHIVE_PATH=$(ls "${SRC_DIR}/${ARCHIVE}" 2>/dev/null || true)
if [ -z "${ARCHIVE_PATH}" ]; then
echo "[node][ERROR] Artifact not found for Node ${VERSION}"
exit 1
fi
echo "[node] Extracting ${ARCHIVE_PATH}"
mkdir -p "${DEST_DIR}"
tar -xJf "${ARCHIVE_PATH}" -C "${DEST_DIR}" --strip-components=1
fi
# Update stable symlink
ln -sfn "${DEST_DIR}" "${RUNTIME_ROOT}/current"
# Permissions sanity
chmod -R 755 "${DEST_DIR}"
echo "[node] Node ${VERSION} installed successfully"

View File

@ -1,12 +1,72 @@
#!/usr/bin/env bash
set -e
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../workspace.sh"
echo "[python] starting python devcontainer install"
echo "[devcontainer:python] installing python"
# --------------------------------------------------
# Config
# --------------------------------------------------
RUNTIME_ROOT="/opt/zlh/runtime/python"
ARTIFACT_ROOT="/opt/zlh/devcontainer/python"
apt update
apt install -y python3 python3-pip python3-venv
VERSION="${RUNTIME_VERSION:-3.12}"
echo "[devcontainer:python] install complete"
SRC_DIR="${ARTIFACT_ROOT}/${VERSION}"
DEST_DIR="${RUNTIME_ROOT}/${VERSION}"
SYMLINK="${RUNTIME_ROOT}/current"
# --------------------------------------------------
# Guards
# --------------------------------------------------
if [ ! -d "${SRC_DIR}" ]; then
echo "[python][ERROR] artifact directory not found: ${SRC_DIR}"
exit 1
fi
if [ -x "${DEST_DIR}/bin/python3" ]; then
echo "[python] python ${VERSION} already installed"
else
ARCHIVE="$(ls ${SRC_DIR}/Python-*.tgz 2>/dev/null | head -n1)"
if [ -z "${ARCHIVE}" ]; then
echo "[python][ERROR] no Python archive found in ${SRC_DIR}"
exit 1
fi
echo "[python] building python ${VERSION}"
mkdir -p "${DEST_DIR}"
BUILD_DIR="$(mktemp -d)"
tar -xzf "${ARCHIVE}" -C "${BUILD_DIR}"
PY_SRC="$(find ${BUILD_DIR} -maxdepth 1 -type d -name 'Python-*' | head -n1)"
if [ -z "${PY_SRC}" ]; then
echo "[python][ERROR] failed to locate extracted python source"
exit 1
fi
cd "${PY_SRC}"
./configure \
--prefix="${DEST_DIR}" \
--enable-optimizations \
--with-ensurepip=install
make -j"$(nproc)"
make install
cd /
rm -rf "${BUILD_DIR}"
fi
# --------------------------------------------------
# Symlink current
# --------------------------------------------------
ln -sfn "${DEST_DIR}" "${SYMLINK}"
# --------------------------------------------------
# Verify
# --------------------------------------------------
"${SYMLINK}/bin/python3" --version
"${SYMLINK}/bin/pip3" --version
echo "[python] python ${VERSION} install complete"

BIN
zlh-agent

Binary file not shown.