196 lines
4.9 KiB
Go
196 lines
4.9 KiB
Go
package devcontainer
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"zlh-agent/internal/provcommon"
|
|
"zlh-agent/internal/state"
|
|
)
|
|
|
|
/*
|
|
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"
|
|
|
|
// WorkspaceDir is the root working directory exposed to dev containers.
|
|
WorkspaceDir = "/home/dev/workspace"
|
|
|
|
// Dev user environment
|
|
DevUser = "dev"
|
|
DevHome = "/home/dev"
|
|
|
|
// CatalogRelativePath is the artifact-server path to the dev runtime catalog.
|
|
CatalogRelativePath = "devcontainer/_catalog.json"
|
|
|
|
// RuntimeRoot is where versioned dev runtimes are installed.
|
|
RuntimeRoot = "/opt/zlh/runtimes"
|
|
|
|
// ReadyMarker is written after a dev container is fully provisioned.
|
|
ReadyMarker = "devcontainer_ready.json"
|
|
)
|
|
|
|
type Catalog struct {
|
|
Runtimes []CatalogRuntime `json:"runtimes"`
|
|
}
|
|
|
|
type CatalogRuntime struct {
|
|
ID string `json:"id"`
|
|
Versions []string `json:"versions"`
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// EnsureWorkspace makes sure the dev workspace root exists before shell/filesystem access.
|
|
func EnsureWorkspace() error {
|
|
return os.MkdirAll(WorkspaceDir, 0755)
|
|
}
|
|
|
|
func LoadCatalog() (*Catalog, error) {
|
|
url := provcommon.BuildArtifactURL(CatalogRelativePath)
|
|
resp, err := (&http.Client{Timeout: 15 * time.Second}).Get(url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetch dev runtime catalog: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("fetch dev runtime catalog: status %d", resp.StatusCode)
|
|
}
|
|
|
|
raw, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read dev runtime catalog: %w", err)
|
|
}
|
|
|
|
var catalog Catalog
|
|
if err := json.Unmarshal(raw, &catalog); err != nil {
|
|
return nil, fmt.Errorf("parse dev runtime catalog: %w", err)
|
|
}
|
|
return &catalog, nil
|
|
}
|
|
|
|
func ValidateRuntimeSelection(cfg state.Config) error {
|
|
catalog, err := LoadCatalog()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
runtimeID := strings.ToLower(strings.TrimSpace(cfg.Runtime))
|
|
version := strings.TrimSpace(cfg.Version)
|
|
if runtimeID == "" {
|
|
return fmt.Errorf("dev runtime missing")
|
|
}
|
|
if version == "" {
|
|
return fmt.Errorf("dev runtime version missing")
|
|
}
|
|
|
|
for _, runtime := range catalog.Runtimes {
|
|
if strings.EqualFold(runtime.ID, runtimeID) {
|
|
for _, candidate := range runtime.Versions {
|
|
if candidate == version {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("unsupported %s version: %s", runtimeID, version)
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("unsupported dev runtime: %s", runtimeID)
|
|
}
|
|
|
|
func RuntimeInstallDir(runtimeName, version string) string {
|
|
return filepath.Join(RuntimeRoot, strings.ToLower(strings.TrimSpace(runtimeName)), strings.TrimSpace(version))
|
|
}
|
|
|
|
func RuntimeInstalled(runtimeName, version string) bool {
|
|
info, err := os.Stat(RuntimeInstallDir(runtimeName, version))
|
|
return err == nil && info.IsDir()
|
|
}
|
|
|
|
func RuntimeMarker(runtimeName, version string) string {
|
|
return fmt.Sprintf("devcontainer-%s-%s", strings.ToLower(strings.TrimSpace(runtimeName)), strings.ReplaceAll(strings.TrimSpace(version), "/", "_"))
|
|
}
|
|
|
|
func EnsureDevUserEnvironment() error {
|
|
if _, err := user.Lookup(DevUser); err != nil {
|
|
if err := exec.Command("useradd", "-m", "-s", "/bin/bash", DevUser).Run(); err != nil {
|
|
return fmt.Errorf("create dev user: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := os.MkdirAll(WorkspaceDir, 0755); err != nil {
|
|
return fmt.Errorf("create workspace: %w", err)
|
|
}
|
|
|
|
u, err := user.Lookup(DevUser)
|
|
if err != nil {
|
|
return fmt.Errorf("lookup dev user: %w", err)
|
|
}
|
|
uid, err := strconv.Atoi(u.Uid)
|
|
if err != nil {
|
|
return fmt.Errorf("parse dev uid: %w", err)
|
|
}
|
|
gid, err := strconv.Atoi(u.Gid)
|
|
if err != nil {
|
|
return fmt.Errorf("parse dev gid: %w", err)
|
|
}
|
|
|
|
if err := filepath.Walk(DevHome, func(path string, info os.FileInfo, walkErr error) error {
|
|
if walkErr != nil {
|
|
return walkErr
|
|
}
|
|
return os.Chown(path, uid, gid)
|
|
}); err != nil {
|
|
return fmt.Errorf("chown dev home: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|