orama/pkg/cli/sandbox/rollout.go
anonpenguin23 6468019136 feat(sandbox): optimize archive upload via server-to-server fanout
- add WithNoHostKeyCheck option for ephemeral server IPs
- upload binary to genesis then distribute to other nodes (faster)
- improve provisioning error handling for cleanup on partial failure
2026-03-07 14:27:09 +02:00

138 lines
4.0 KiB
Go

package sandbox
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/DeBrosOfficial/network/pkg/cli/remotessh"
"github.com/DeBrosOfficial/network/pkg/inspector"
)
// Rollout builds, pushes, and performs a rolling upgrade on a sandbox cluster.
func Rollout(name string) error {
cfg, err := LoadConfig()
if err != nil {
return err
}
state, err := resolveSandbox(name)
if err != nil {
return err
}
sshKeyPath := cfg.ExpandedPrivateKeyPath()
fmt.Printf("Rolling out to sandbox %q (%d nodes)\n\n", state.Name, len(state.Servers))
// Step 1: Find or require binary archive
archivePath := findNewestArchive()
if archivePath == "" {
return fmt.Errorf("no binary archive found in /tmp/ (run `orama build` first)")
}
info, _ := os.Stat(archivePath)
fmt.Printf("Archive: %s (%s)\n\n", filepath.Base(archivePath), formatBytes(info.Size()))
// Step 2: Push archive to all nodes
fmt.Println("Pushing archive to all nodes...")
remotePath := "/tmp/" + filepath.Base(archivePath)
for i, srv := range state.Servers {
node := inspector.Node{User: "root", Host: srv.IP, SSHKey: sshKeyPath}
fmt.Printf(" [%d/%d] Uploading to %s...\n", i+1, len(state.Servers), srv.Name)
if err := remotessh.UploadFile(node, archivePath, remotePath, remotessh.WithNoHostKeyCheck()); err != nil {
return fmt.Errorf("upload to %s: %w", srv.Name, err)
}
// Extract archive
extractCmd := fmt.Sprintf("mkdir -p /opt/orama && tar xzf %s -C /opt/orama && rm -f %s",
remotePath, remotePath)
if err := remotessh.RunSSHStreaming(node, extractCmd, remotessh.WithNoHostKeyCheck()); err != nil {
return fmt.Errorf("extract on %s: %w", srv.Name, err)
}
}
// Step 3: Rolling upgrade — followers first, leader last
fmt.Println("\nRolling upgrade (followers first, leader last)...")
// Find the leader
leaderIdx := findLeaderIndex(state, sshKeyPath)
if leaderIdx < 0 {
fmt.Fprintf(os.Stderr, " Warning: could not detect RQLite leader, upgrading in order\n")
}
// Upgrade non-leaders first
for i, srv := range state.Servers {
if i == leaderIdx {
continue // skip leader, do it last
}
if err := upgradeNode(srv, sshKeyPath, i+1, len(state.Servers)); err != nil {
return err
}
// Wait between nodes
if i < len(state.Servers)-1 {
fmt.Printf(" Waiting 15s before next node...\n")
time.Sleep(15 * time.Second)
}
}
// Upgrade leader last
if leaderIdx >= 0 {
srv := state.Servers[leaderIdx]
if err := upgradeNode(srv, sshKeyPath, len(state.Servers), len(state.Servers)); err != nil {
return err
}
}
fmt.Printf("\nRollout complete for sandbox %q\n", state.Name)
return nil
}
// findLeaderIndex returns the index of the RQLite leader node, or -1 if unknown.
func findLeaderIndex(state *SandboxState, sshKeyPath string) int {
for i, srv := range state.Servers {
node := inspector.Node{User: "root", Host: srv.IP, SSHKey: sshKeyPath}
out, err := runSSHOutput(node, "curl -sf http://localhost:5001/status 2>/dev/null | grep -o '\"state\":\"[^\"]*\"'")
if err == nil && contains(out, "Leader") {
return i
}
}
return -1
}
// upgradeNode performs `orama node upgrade --restart` on a single node.
func upgradeNode(srv ServerState, sshKeyPath string, current, total int) error {
node := inspector.Node{User: "root", Host: srv.IP, SSHKey: sshKeyPath}
fmt.Printf(" [%d/%d] Upgrading %s (%s)...\n", current, total, srv.Name, srv.IP)
if err := remotessh.RunSSHStreaming(node, "orama node upgrade --restart", remotessh.WithNoHostKeyCheck()); err != nil {
return fmt.Errorf("upgrade %s: %w", srv.Name, err)
}
// Wait for health
fmt.Printf(" Checking health...")
if err := waitForRQLiteHealth(node, 2*time.Minute); err != nil {
fmt.Printf(" WARN: %v\n", err)
} else {
fmt.Println(" OK")
}
return nil
}
// contains checks if s contains substr.
func contains(s, substr string) bool {
return len(s) >= len(substr) && findSubstring(s, substr)
}
func findSubstring(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}