Compare commits
4 Commits
v0.1.0-dev
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 46637f6759 | |||
| 9c8e3a2b22 | |||
| 463baf80e0 | |||
| 5319b594e0 |
@ -12,6 +12,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"zlh-agent/internal/provision"
|
"zlh-agent/internal/provision"
|
||||||
|
"zlh-agent/internal/provision/devcontainer"
|
||||||
|
"zlh-agent/internal/provision/devcontainer/go"
|
||||||
|
"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/provision/minecraft"
|
||||||
"zlh-agent/internal/state"
|
"zlh-agent/internal/state"
|
||||||
"zlh-agent/internal/system"
|
"zlh-agent/internal/system"
|
||||||
@ -37,14 +42,13 @@ func runProvisionPipeline(cfg *state.Config) error {
|
|||||||
state.SetState(state.StateInstalling)
|
state.SetState(state.StateInstalling)
|
||||||
state.SetInstallStep("provision_all")
|
state.SetInstallStep("provision_all")
|
||||||
|
|
||||||
// Installer (downloads files, patches, configs, etc.)
|
|
||||||
if err := provision.ProvisionAll(*cfg); err != nil {
|
if err := provision.ProvisionAll(*cfg); err != nil {
|
||||||
state.SetError(err)
|
state.SetError(err)
|
||||||
state.SetState(state.StateError)
|
state.SetState(state.StateError)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extra Minecraft verification
|
// Extra Minecraft verification ONLY for Minecraft
|
||||||
if strings.ToLower(cfg.Game) == "minecraft" {
|
if strings.ToLower(cfg.Game) == "minecraft" {
|
||||||
if err := minecraft.VerifyMinecraftInstallWithRepair(*cfg); err != nil {
|
if err := minecraft.VerifyMinecraftInstallWithRepair(*cfg); err != nil {
|
||||||
state.SetError(err)
|
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 {
|
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)
|
dir := provision.ServerDir(*cfg)
|
||||||
game := strings.ToLower(cfg.Game)
|
game := strings.ToLower(cfg.Game)
|
||||||
variant := strings.ToLower(cfg.Variant)
|
variant := strings.ToLower(cfg.Variant)
|
||||||
@ -82,7 +120,7 @@ func ensureProvisioned(cfg *state.Config) error {
|
|||||||
libraries := filepath.Join(dir, "libraries")
|
libraries := filepath.Join(dir, "libraries")
|
||||||
|
|
||||||
if fileExists(runSh) && dirExists(libraries) {
|
if fileExists(runSh) && dirExists(libraries) {
|
||||||
return nil // already provisioned
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return runProvisionPipeline(cfg)
|
return runProvisionPipeline(cfg)
|
||||||
@ -91,7 +129,7 @@ func ensureProvisioned(cfg *state.Config) error {
|
|||||||
// ---------- VANILLA / PAPER / PURPUR / FABRIC ----------
|
// ---------- VANILLA / PAPER / PURPUR / FABRIC ----------
|
||||||
jar := filepath.Join(dir, "server.jar")
|
jar := filepath.Join(dir, "server.jar")
|
||||||
if fileExists(jar) {
|
if fileExists(jar) {
|
||||||
return nil // already provisioned
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return runProvisionPipeline(cfg)
|
return runProvisionPipeline(cfg)
|
||||||
@ -119,7 +157,6 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run provision + server start asynchronously
|
|
||||||
go func(c state.Config) {
|
go func(c state.Config) {
|
||||||
log.Println("[agent] async provision+start begin")
|
log.Println("[agent] async provision+start begin")
|
||||||
|
|
||||||
@ -128,15 +165,18 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := system.StartServer(&c); err != nil {
|
// Dev containers may not start a server process
|
||||||
log.Println("[agent] start error:", err)
|
if c.ContainerType != "dev" {
|
||||||
state.SetError(err)
|
if err := system.StartServer(&c); err != nil {
|
||||||
state.SetState(state.StateError)
|
log.Println("[agent] start error:", err)
|
||||||
return
|
state.SetError(err)
|
||||||
|
state.SetState(state.StateError)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("[agent] async provision+start complete")
|
log.Println("[agent] async provision+start complete")
|
||||||
}(*&cfg)
|
}(cfg)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusAccepted)
|
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) {
|
func handleStart(w http.ResponseWriter, r *http.Request) {
|
||||||
cfg, err := state.LoadConfig()
|
cfg, err := state.LoadConfig()
|
||||||
@ -153,6 +193,11 @@ func handleStart(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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 {
|
if err := system.StartServer(cfg); err != nil {
|
||||||
http.Error(w, "start error: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "start error: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@ -183,6 +228,11 @@ func handleRestart(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.ContainerType == "dev" {
|
||||||
|
http.Error(w, "dev containers do not support restart", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
_ = system.StopServer()
|
_ = system.StopServer()
|
||||||
|
|
||||||
if err := system.StartServer(cfg); err != nil {
|
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 {
|
func NewMux() *http.ServeMux {
|
||||||
m := http.NewServeMux()
|
m := http.NewServeMux()
|
||||||
|
|||||||
35
internal/provision/addons/addons.go
Normal file
35
internal/provision/addons/addons.go
Normal 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
|
||||||
|
}
|
||||||
31
internal/provision/addons/codeserver/install.go
Normal file
31
internal/provision/addons/codeserver/install.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package codeserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"zlh-agent/internal/provision/executil"
|
||||||
|
"zlh-agent/internal/provision/markers"
|
||||||
|
"zlh-agent/internal/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Install(cfg state.Config) error {
|
||||||
|
const marker = "addon-codeserver"
|
||||||
|
|
||||||
|
if markers.IsPresent(marker) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptPath := filepath.Join(
|
||||||
|
executil.ScriptsRoot,
|
||||||
|
"addons",
|
||||||
|
"codeserver",
|
||||||
|
"install.sh",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := executil.RunScript(scriptPath); err != nil {
|
||||||
|
return fmt.Errorf("codeserver install failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return markers.Write(marker)
|
||||||
|
}
|
||||||
22
internal/provision/addons/codeserver/verify.go
Normal file
22
internal/provision/addons/codeserver/verify.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package codeserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
const marker = "/opt/zlh/.zlh/addons/code-server.installed"
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
24
internal/provision/addons/markers.go
Normal file
24
internal/provision/addons/markers.go
Normal 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)
|
||||||
|
}
|
||||||
56
internal/provision/devcontainer/common.go
Normal file
56
internal/provision/devcontainer/common.go
Normal 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)
|
||||||
|
}
|
||||||
30
internal/provision/devcontainer/devcontainer.go
Normal file
30
internal/provision/devcontainer/devcontainer.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Provision(cfg state.Config) error {
|
||||||
|
runtime := strings.ToLower(cfg.Runtime)
|
||||||
|
|
||||||
|
switch runtime {
|
||||||
|
case "node":
|
||||||
|
return node.Install(cfg)
|
||||||
|
case "python":
|
||||||
|
return python.Install(cfg)
|
||||||
|
case "go":
|
||||||
|
return devgo.Install(cfg)
|
||||||
|
case "java":
|
||||||
|
return java.Install(cfg)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported dev container runtime: %s", runtime)
|
||||||
|
}
|
||||||
|
}
|
||||||
31
internal/provision/devcontainer/go/install.go
Normal file
31
internal/provision/devcontainer/go/install.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package goenv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"zlh-agent/internal/provision/executil"
|
||||||
|
"zlh-agent/internal/provision/markers"
|
||||||
|
"zlh-agent/internal/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Install(cfg state.Config) error {
|
||||||
|
const marker = "devcontainer-go"
|
||||||
|
|
||||||
|
if markers.IsPresent(marker) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptPath := filepath.Join(
|
||||||
|
executil.ScriptsRoot,
|
||||||
|
"devcontainer",
|
||||||
|
"go",
|
||||||
|
"install.sh",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := executil.RunScript(scriptPath); err != nil {
|
||||||
|
return fmt.Errorf("go devcontainer install failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return markers.Write(marker)
|
||||||
|
}
|
||||||
15
internal/provision/devcontainer/go/verify.go
Normal file
15
internal/provision/devcontainer/go/verify.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package goenv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"zlh-agent/internal/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Verify(cfg state.Config) error {
|
||||||
|
if _, err := exec.LookPath("go"); err != nil {
|
||||||
|
return fmt.Errorf("go binary not found in PATH")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
30
internal/provision/devcontainer/java/install.go
Normal file
30
internal/provision/devcontainer/java/install.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package java
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"zlh-agent/internal/provision/executil"
|
||||||
|
"zlh-agent/internal/provision/markers"
|
||||||
|
"zlh-agent/internal/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Install(cfg state.Config) error {
|
||||||
|
|
||||||
|
if markers.IsPresent("devcontainer-java") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptPath := filepath.Join(
|
||||||
|
executil.ScriptsRoot,
|
||||||
|
"devcontainer",
|
||||||
|
"java",
|
||||||
|
"install.sh",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := executil.RunScript(scriptPath); err != nil {
|
||||||
|
return fmt.Errorf("java devcontainer install failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return markers.Write("devcontainer-java")
|
||||||
|
}
|
||||||
15
internal/provision/devcontainer/java/verify.go
Normal file
15
internal/provision/devcontainer/java/verify.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package java
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"zlh-agent/internal/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Verify(cfg state.Config) error {
|
||||||
|
if _, err := exec.LookPath("java"); err != nil {
|
||||||
|
return fmt.Errorf("java binary not found in PATH")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
31
internal/provision/devcontainer/node/install.go
Normal file
31
internal/provision/devcontainer/node/install.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"zlh-agent/internal/provision/executil"
|
||||||
|
"zlh-agent/internal/provision/markers"
|
||||||
|
"zlh-agent/internal/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Install(cfg state.Config) error {
|
||||||
|
const marker = "devcontainer-node"
|
||||||
|
|
||||||
|
if markers.IsPresent(marker) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptPath := filepath.Join(
|
||||||
|
executil.ScriptsRoot,
|
||||||
|
"devcontainer",
|
||||||
|
"node",
|
||||||
|
"install.sh",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := executil.RunScript(scriptPath); err != nil {
|
||||||
|
return fmt.Errorf("node devcontainer install failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return markers.Write(marker)
|
||||||
|
}
|
||||||
22
internal/provision/devcontainer/node/verify.go
Normal file
22
internal/provision/devcontainer/node/verify.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"zlh-agent/internal/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Verify(cfg state.Config) error {
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := exec.LookPath("npm"); err != nil {
|
||||||
|
return fmt.Errorf("npm binary not found in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
31
internal/provision/devcontainer/python/install.go
Normal file
31
internal/provision/devcontainer/python/install.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package python
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"zlh-agent/internal/provision/executil"
|
||||||
|
"zlh-agent/internal/provision/markers"
|
||||||
|
"zlh-agent/internal/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Install(cfg state.Config) error {
|
||||||
|
const marker = "devcontainer-python"
|
||||||
|
|
||||||
|
if markers.IsPresent(marker) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptPath := filepath.Join(
|
||||||
|
executil.ScriptsRoot,
|
||||||
|
"devcontainer",
|
||||||
|
"python",
|
||||||
|
"install.sh",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := executil.RunScript(scriptPath); err != nil {
|
||||||
|
return fmt.Errorf("python devcontainer install failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return markers.Write(marker)
|
||||||
|
}
|
||||||
18
internal/provision/devcontainer/python/verify.go
Normal file
18
internal/provision/devcontainer/python/verify.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package python
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"zlh-agent/internal/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Verify(cfg state.Config) error {
|
||||||
|
if _, err := exec.LookPath("python3"); err != nil {
|
||||||
|
return fmt.Errorf("python3 binary not found in PATH")
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("pip3"); err != nil {
|
||||||
|
return fmt.Errorf("pip3 binary not found in PATH")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
15
internal/provision/executil/executil.go
Normal file
15
internal/provision/executil/executil.go
Normal 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()
|
||||||
|
}
|
||||||
22
internal/provision/markers/markers.go
Normal file
22
internal/provision/markers/markers.go
Normal 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)
|
||||||
|
}
|
||||||
@ -5,117 +5,139 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"zlh-agent/internal/state"
|
"zlh-agent/internal/state"
|
||||||
|
"zlh-agent/internal/provision/addons"
|
||||||
|
"zlh-agent/internal/provision/devcontainer"
|
||||||
"zlh-agent/internal/provision/minecraft"
|
"zlh-agent/internal/provision/minecraft"
|
||||||
"zlh-agent/internal/provision/steam"
|
"zlh-agent/internal/provision/steam"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
ProvisionAll — unified entrypoint for MC + Steam.
|
ProvisionAll — unified entrypoint for provisioning.
|
||||||
IMPORTANT:
|
IMPORTANT:
|
||||||
- This function ONLY performs installation.
|
- This function ONLY performs installation.
|
||||||
- Validation/verification happens in ensureProvisioned().
|
- Validation/verification happens in ensureProvisioned().
|
||||||
|
- state.Config is treated as immutable desired state.
|
||||||
*/
|
*/
|
||||||
func ProvisionAll(cfg state.Config) error {
|
func ProvisionAll(cfg state.Config) error {
|
||||||
|
|
||||||
game := strings.ToLower(cfg.Game)
|
|
||||||
variant := strings.ToLower(cfg.Variant)
|
|
||||||
|
|
||||||
/* ---------------------------------------------------------
|
/* ---------------------------------------------------------
|
||||||
MINECRAFT
|
BASE ROLE PROVISIONING
|
||||||
--------------------------------------------------------- */
|
--------------------------------------------------------- */
|
||||||
if game == "minecraft" {
|
if cfg.ContainerType == "dev" {
|
||||||
|
|
||||||
// 1. Install Java (runtime)
|
if err := devcontainer.Provision(cfg); err != nil {
|
||||||
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DO NOT VERIFY HERE.
|
} else {
|
||||||
// Verification happens in ensureProvisioned(), AFTER install.
|
|
||||||
return nil
|
// 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) {
|
if len(cfg.Addons) > 0 {
|
||||||
|
if err := addons.Provision(cfg); err != nil {
|
||||||
// 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DO NOT VERIFY HERE (Steam verification TBD later)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------
|
return nil
|
||||||
UNKNOWN GAME TYPE
|
|
||||||
--------------------------------------------------------- */
|
|
||||||
return fmt.Errorf("unsupported game type: %s", game)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,17 @@ import (
|
|||||||
----------------------------------------------------------------------------*/
|
----------------------------------------------------------------------------*/
|
||||||
|
|
||||||
type Config struct {
|
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"`
|
Game string `json:"game"`
|
||||||
Variant string `json:"variant"`
|
Variant string `json:"variant"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
@ -32,6 +42,8 @@ type Config struct {
|
|||||||
AdminPass string `json:"admin_pass,omitempty"`
|
AdminPass string `json:"admin_pass,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* --------------------------------------------------------------------------
|
/* --------------------------------------------------------------------------
|
||||||
AGENT STATE ENUM
|
AGENT STATE ENUM
|
||||||
----------------------------------------------------------------------------*/
|
----------------------------------------------------------------------------*/
|
||||||
|
|||||||
60
scripts/devcontainer/codeserver/install.sh
Normal file
60
scripts/devcontainer/codeserver/install.sh
Normal 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"
|
||||||
51
scripts/devcontainer/go/install.sh
Normal file
51
scripts/devcontainer/go/install.sh
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "[go] starting go devcontainer install"
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Config
|
||||||
|
# --------------------------------------------------
|
||||||
|
RUNTIME_ROOT="/opt/zlh/runtime/go"
|
||||||
|
ARTIFACT_ROOT="/opt/zlh/devcontainer/go"
|
||||||
|
|
||||||
|
VERSION="${RUNTIME_VERSION:-1.25}"
|
||||||
|
|
||||||
|
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"
|
||||||
62
scripts/devcontainer/java/install.sh
Normal file
62
scripts/devcontainer/java/install.sh
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "[java] starting java devcontainer install"
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Config
|
||||||
|
# --------------------------------------------------
|
||||||
|
RUNTIME_ROOT="/opt/zlh/runtime/java"
|
||||||
|
ARTIFACT_ROOT="/opt/zlh/devcontainer/java"
|
||||||
|
|
||||||
|
VERSION="${RUNTIME_VERSION:-17}"
|
||||||
|
|
||||||
|
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"
|
||||||
40
scripts/devcontainer/node/install.sh
Normal file
40
scripts/devcontainer/node/install.sh
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RUNTIME_ROOT="/opt/zlh/runtime/node"
|
||||||
|
ARTIFACT_ROOT="/opt/zlh/devcontainer/node"
|
||||||
|
|
||||||
|
VERSION="${RUNTIME_VERSION:-24}"
|
||||||
|
|
||||||
|
ARCHIVE="node-v${VERSION}.*-linux-x64.tar.xz"
|
||||||
|
SRC_DIR="${ARTIFACT_ROOT}/${VERSION}"
|
||||||
|
DEST_DIR="${RUNTIME_ROOT}/${VERSION}"
|
||||||
|
|
||||||
|
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"
|
||||||
72
scripts/devcontainer/python/install.sh
Normal file
72
scripts/devcontainer/python/install.sh
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "[python] starting python devcontainer install"
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Config
|
||||||
|
# --------------------------------------------------
|
||||||
|
RUNTIME_ROOT="/opt/zlh/runtime/python"
|
||||||
|
ARTIFACT_ROOT="/opt/zlh/devcontainer/python"
|
||||||
|
|
||||||
|
VERSION="${RUNTIME_VERSION:-3.12}"
|
||||||
|
|
||||||
|
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"
|
||||||
22
scripts/workspace.sh
Normal file
22
scripts/workspace.sh
Normal 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})"
|
||||||
Loading…
Reference in New Issue
Block a user