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>
191 lines
4.3 KiB
Go
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()
|
|
}
|