127 lines
4.0 KiB
Go
127 lines
4.0 KiB
Go
package main
|
|
|
|
// pi-config.go — re-sync the user's pi config (dotfiles, mounted read-only)
|
|
// into the image build context and rebuild the worker + ops images, so
|
|
// spawned agents pick up config changes without a full deploy.
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
envPiDotfilesMount string = "LVMH_PI_DOTFILES" // default /pi-dotfiles
|
|
piSyncScript string = "/app/build/deploy/rsync-pi-agent.sh"
|
|
piSyncTimeout time.Duration = 10 * time.Minute
|
|
)
|
|
|
|
// piSyncState serializes resyncs; at most one runs at a time.
|
|
var piSyncState = struct {
|
|
mu sync.Mutex
|
|
running bool
|
|
}{}
|
|
|
|
func (s *Server) handlePiConfigResync(w http.ResponseWriter, r *http.Request) {
|
|
piSyncState.mu.Lock()
|
|
if piSyncState.running {
|
|
piSyncState.mu.Unlock()
|
|
writeError(w, http.StatusConflict, "pi config sync already running")
|
|
return
|
|
}
|
|
piSyncState.running = true
|
|
piSyncState.mu.Unlock()
|
|
defer func() {
|
|
piSyncState.mu.Lock()
|
|
piSyncState.running = false
|
|
piSyncState.mu.Unlock()
|
|
}()
|
|
|
|
src := envOr(envPiDotfilesMount, "/pi-dotfiles")
|
|
if _, err := os.Stat(filepath.Join(src, "settings.json")); err != nil {
|
|
writeError(w, http.StatusBadRequest, "no pi settings at "+src)
|
|
return
|
|
}
|
|
scriptPath := os.Getenv("LVMH_TEST_SYNC_SCRIPT")
|
|
if scriptPath == "" {
|
|
scriptPath = piSyncScript
|
|
}
|
|
if _, err := os.Stat(scriptPath); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "sync script missing: "+scriptPath)
|
|
return
|
|
}
|
|
|
|
// The script writes into the repo checkout (docker/pi-agent) and we then
|
|
// rebuild both images from that context. Build context is mounted :ro in
|
|
// compose — remount rw or run against a writable copy? The build context
|
|
// bind is :ro; run the script with DEST inside the shared lvmh-data volume
|
|
// instead, then build from a tar we assemble there.
|
|
bakeDir := os.Getenv("LVMH_PI_BAKE_DIR")
|
|
if bakeDir == "" {
|
|
bakeDir = "/data/pi-bake"
|
|
}
|
|
script := exec.CommandContext(r.Context(), "bash", scriptPath)
|
|
script.Env = append(os.Environ(),
|
|
"LVMH_PI_AGENT_DIR="+src,
|
|
"LVMH_PI_BAKE_DIR="+bakeDir,
|
|
)
|
|
out, err := script.CombinedOutput()
|
|
if err != nil {
|
|
log.Printf("pi-config: sync failed: %v\n%s", err, out)
|
|
writeError(w, http.StatusInternalServerError, "sync failed: "+err.Error())
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), piSyncTimeout)
|
|
defer cancel()
|
|
bakeCtx := filepath.Join(os.TempDir(), "lvmh-pi-bake-ctx")
|
|
if bc := os.Getenv("LVMH_TEST_BUILD_CTX"); bc != "" {
|
|
bakeCtx = bc
|
|
}
|
|
if err := os.RemoveAll(bakeCtx); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "clear bake context: "+err.Error())
|
|
return
|
|
}
|
|
buildRepo := "/app/build"
|
|
if br := os.Getenv("LVMH_TEST_BUILD_REPO"); br != "" {
|
|
buildRepo = br
|
|
}
|
|
if err := copyTree(buildRepo, bakeCtx); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "prep build context: "+err.Error())
|
|
return
|
|
}
|
|
if err := os.RemoveAll(filepath.Join(bakeCtx, "docker", "pi-agent")); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "clear stale pi-agent: "+err.Error())
|
|
return
|
|
}
|
|
if err := os.Rename(bakeDir, filepath.Join(bakeCtx, "docker", "pi-agent")); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "move bake dir: "+err.Error())
|
|
return
|
|
}
|
|
|
|
if err := s.spawn.buildImage(ctx, bakeCtx, filepath.Join(bakeCtx, "docker", "worker.Dockerfile"), imageRefWorker); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "worker rebuild: "+err.Error())
|
|
return
|
|
}
|
|
if err := s.spawn.buildImage(ctx, bakeCtx, filepath.Join(bakeCtx, "docker", "control.Dockerfile"), opsImageRef); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "ops rebuild: "+err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|
|
|
|
// copyTree duplicates a directory tree for a one-shot build context.
|
|
func copyTree(src, dst string) error {
|
|
out, err := exec.Command("cp", "-a", src, dst).CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("cp %s -> %s: %s", src, dst, out)
|
|
}
|
|
return nil
|
|
}
|