mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 18:36:57 +00:00
212 lines
5.1 KiB
Go
212 lines
5.1 KiB
Go
package sandbox
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/DeBrosOfficial/network/pkg/inspector"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// SandboxStatus represents the lifecycle state of a sandbox.
|
|
type SandboxStatus string
|
|
|
|
const (
|
|
StatusCreating SandboxStatus = "creating"
|
|
StatusRunning SandboxStatus = "running"
|
|
StatusDestroying SandboxStatus = "destroying"
|
|
StatusError SandboxStatus = "error"
|
|
)
|
|
|
|
// SandboxState holds the full state of an active sandbox cluster.
|
|
type SandboxState struct {
|
|
Name string `yaml:"name"`
|
|
CreatedAt time.Time `yaml:"created_at"`
|
|
Domain string `yaml:"domain"`
|
|
Status SandboxStatus `yaml:"status"`
|
|
Servers []ServerState `yaml:"servers"`
|
|
}
|
|
|
|
// ServerState holds the state of a single server in the sandbox.
|
|
type ServerState struct {
|
|
ID int64 `yaml:"id"` // Hetzner server ID
|
|
Name string `yaml:"name"` // e.g., sbx-feature-webrtc-1
|
|
IP string `yaml:"ip"` // Public IPv4
|
|
Role string `yaml:"role"` // "nameserver" or "node"
|
|
FloatingIP string `yaml:"floating_ip,omitempty"` // Only for nameserver nodes
|
|
WgIP string `yaml:"wg_ip,omitempty"` // WireGuard IP (populated after install)
|
|
}
|
|
|
|
// sandboxesDir returns ~/.orama/sandboxes/, creating it if needed.
|
|
func sandboxesDir() (string, error) {
|
|
dir, err := configDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
sbxDir := filepath.Join(dir, "sandboxes")
|
|
if err := os.MkdirAll(sbxDir, 0700); err != nil {
|
|
return "", fmt.Errorf("create sandboxes directory: %w", err)
|
|
}
|
|
return sbxDir, nil
|
|
}
|
|
|
|
// statePath returns the path for a sandbox's state file.
|
|
func statePath(name string) (string, error) {
|
|
dir, err := sandboxesDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(dir, name+".yaml"), nil
|
|
}
|
|
|
|
// SaveState persists the sandbox state to disk.
|
|
func SaveState(state *SandboxState) error {
|
|
path, err := statePath(state.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
data, err := yaml.Marshal(state)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal state: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(path, data, 0600); err != nil {
|
|
return fmt.Errorf("write state: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadState reads a sandbox state from disk.
|
|
func LoadState(name string) (*SandboxState, error) {
|
|
path, err := statePath(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("sandbox %q not found", name)
|
|
}
|
|
return nil, fmt.Errorf("read state: %w", err)
|
|
}
|
|
|
|
var state SandboxState
|
|
if err := yaml.Unmarshal(data, &state); err != nil {
|
|
return nil, fmt.Errorf("parse state: %w", err)
|
|
}
|
|
|
|
return &state, nil
|
|
}
|
|
|
|
// DeleteState removes the sandbox state file.
|
|
func DeleteState(name string) error {
|
|
path, err := statePath(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("delete state: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListStates returns all sandbox states from disk.
|
|
func ListStates() ([]*SandboxState, error) {
|
|
dir, err := sandboxesDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read sandboxes directory: %w", err)
|
|
}
|
|
|
|
var states []*SandboxState
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") {
|
|
continue
|
|
}
|
|
name := strings.TrimSuffix(entry.Name(), ".yaml")
|
|
state, err := LoadState(name)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: could not load sandbox %q: %v\n", name, err)
|
|
continue
|
|
}
|
|
states = append(states, state)
|
|
}
|
|
|
|
return states, nil
|
|
}
|
|
|
|
// FindActiveSandbox returns the first sandbox in running or creating state.
|
|
// Returns nil if no active sandbox exists.
|
|
func FindActiveSandbox() (*SandboxState, error) {
|
|
states, err := ListStates()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, s := range states {
|
|
if s.Status == StatusRunning || s.Status == StatusCreating {
|
|
return s, nil
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// ToNodes converts sandbox servers to inspector.Node structs for SSH operations.
|
|
// Sets SSHKey to the provided key path on each node.
|
|
func (s *SandboxState) ToNodes(sshKeyPath string) []inspector.Node {
|
|
nodes := make([]inspector.Node, len(s.Servers))
|
|
for i, srv := range s.Servers {
|
|
nodes[i] = inspector.Node{
|
|
Environment: "sandbox",
|
|
User: "root",
|
|
Host: srv.IP,
|
|
Role: srv.Role,
|
|
SSHKey: sshKeyPath,
|
|
}
|
|
}
|
|
return nodes
|
|
}
|
|
|
|
// NameserverNodes returns only the nameserver nodes.
|
|
func (s *SandboxState) NameserverNodes() []ServerState {
|
|
var ns []ServerState
|
|
for _, srv := range s.Servers {
|
|
if srv.Role == "nameserver" {
|
|
ns = append(ns, srv)
|
|
}
|
|
}
|
|
return ns
|
|
}
|
|
|
|
// RegularNodes returns only the non-nameserver nodes.
|
|
func (s *SandboxState) RegularNodes() []ServerState {
|
|
var nodes []ServerState
|
|
for _, srv := range s.Servers {
|
|
if srv.Role == "node" {
|
|
nodes = append(nodes, srv)
|
|
}
|
|
}
|
|
return nodes
|
|
}
|
|
|
|
// GenesisServer returns the first server (genesis node).
|
|
func (s *SandboxState) GenesisServer() ServerState {
|
|
if len(s.Servers) == 0 {
|
|
return ServerState{}
|
|
}
|
|
return s.Servers[0]
|
|
}
|