feat(connection): Add MachineRegistry for federation support (gt-f9x.9)
Implement MachineRegistry that manages machine configurations and provides Connection instances for local and remote operations: - Machine struct with name, type, host, key path, and town path - Registry with JSON persistence (federation.json) - CRUD operations: Get, Add, Remove, List - Connection factory that returns appropriate Connection type - Built-in "local" machine that cannot be removed SSH connections return an error until SSHConnection is implemented. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
190
internal/connection/registry.go
Normal file
190
internal/connection/registry.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Machine represents a managed machine in the federation.
|
||||
type Machine struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // "local", "ssh"
|
||||
Host string `json:"host"` // for ssh: user@host
|
||||
KeyPath string `json:"key_path"` // SSH private key path
|
||||
TownPath string `json:"town_path"` // Path to town root on remote
|
||||
}
|
||||
|
||||
// registryData is the JSON file structure.
|
||||
type registryData struct {
|
||||
Version int `json:"version"`
|
||||
Machines map[string]*Machine `json:"machines"`
|
||||
}
|
||||
|
||||
// MachineRegistry manages machine configurations and provides Connection instances.
|
||||
type MachineRegistry struct {
|
||||
path string
|
||||
machines map[string]*Machine
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewMachineRegistry creates a registry from the given config file path.
|
||||
// If the file doesn't exist, an empty registry is created.
|
||||
func NewMachineRegistry(configPath string) (*MachineRegistry, error) {
|
||||
r := &MachineRegistry{
|
||||
path: configPath,
|
||||
machines: make(map[string]*Machine),
|
||||
}
|
||||
|
||||
// Load existing config if present
|
||||
if err := r.load(); err != nil && !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("loading registry: %w", err)
|
||||
}
|
||||
|
||||
// Ensure "local" machine always exists
|
||||
if _, ok := r.machines["local"]; !ok {
|
||||
r.machines["local"] = &Machine{
|
||||
Name: "local",
|
||||
Type: "local",
|
||||
}
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// load reads the registry from disk.
|
||||
func (r *MachineRegistry) load() error {
|
||||
data, err := os.ReadFile(r.path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var rd registryData
|
||||
if err := json.Unmarshal(data, &rd); err != nil {
|
||||
return fmt.Errorf("parsing registry: %w", err)
|
||||
}
|
||||
|
||||
r.machines = rd.Machines
|
||||
if r.machines == nil {
|
||||
r.machines = make(map[string]*Machine)
|
||||
}
|
||||
|
||||
// Populate machine names from keys
|
||||
for name, m := range r.machines {
|
||||
m.Name = name
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// save writes the registry to disk.
|
||||
func (r *MachineRegistry) save() error {
|
||||
rd := registryData{
|
||||
Version: 1,
|
||||
Machines: r.machines,
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(rd, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling registry: %w", err)
|
||||
}
|
||||
|
||||
// Ensure parent directory exists
|
||||
dir := filepath.Dir(r.path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("creating config directory: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(r.path, data, fs.FileMode(0644)); err != nil {
|
||||
return fmt.Errorf("writing registry: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns a machine by name.
|
||||
func (r *MachineRegistry) Get(name string) (*Machine, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
m, ok := r.machines[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("machine not found: %s", name)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Add adds or updates a machine in the registry.
|
||||
func (r *MachineRegistry) Add(m *Machine) error {
|
||||
if m.Name == "" {
|
||||
return fmt.Errorf("machine name is required")
|
||||
}
|
||||
if m.Type == "" {
|
||||
return fmt.Errorf("machine type is required")
|
||||
}
|
||||
if m.Type == "ssh" && m.Host == "" {
|
||||
return fmt.Errorf("ssh machine requires host")
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
r.machines[m.Name] = m
|
||||
return r.save()
|
||||
}
|
||||
|
||||
// Remove removes a machine from the registry.
|
||||
func (r *MachineRegistry) Remove(name string) error {
|
||||
if name == "local" {
|
||||
return fmt.Errorf("cannot remove local machine")
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if _, ok := r.machines[name]; !ok {
|
||||
return fmt.Errorf("machine not found: %s", name)
|
||||
}
|
||||
|
||||
delete(r.machines, name)
|
||||
return r.save()
|
||||
}
|
||||
|
||||
// List returns all machines in the registry.
|
||||
func (r *MachineRegistry) List() []*Machine {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
result := make([]*Machine, 0, len(r.machines))
|
||||
for _, m := range r.machines {
|
||||
result = append(result, m)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Connection returns a Connection for the named machine.
|
||||
func (r *MachineRegistry) Connection(name string) (Connection, error) {
|
||||
m, err := r.Get(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch m.Type {
|
||||
case "local":
|
||||
return NewLocalConnection(), nil
|
||||
case "ssh":
|
||||
// SSH connection not yet implemented
|
||||
return nil, fmt.Errorf("ssh connections not yet implemented")
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown machine type: %s", m.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// LocalConnection returns the local connection.
|
||||
// This is a convenience method for the common case.
|
||||
func (r *MachineRegistry) LocalConnection() *LocalConnection {
|
||||
return NewLocalConnection()
|
||||
}
|
||||
Reference in New Issue
Block a user