Files
gastown/internal/connection/registry.go
rictus 0013cc0f19 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>
2026-01-01 19:24:51 -08:00

191 lines
4.3 KiB
Go

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()
}