Files
mcp/internal/registry/db.go
Kyle Isom 714320c018 Add edge routing and health check RPCs (Phase 2)
New agent RPCs for v2 multi-node orchestration:

- SetupEdgeRoute: provisions TLS cert from Metacrypt, resolves backend
  hostname to Tailnet IP, validates it's in 100.64.0.0/10, registers
  L7 route in mc-proxy. Rejects backend_tls=false.
- RemoveEdgeRoute: removes mc-proxy route, cleans up TLS cert, removes
  registry entry.
- ListEdgeRoutes: returns all edge routes with cert serial/expiry.
- HealthCheck: returns agent health and container count.

New database table (migration 4): edge_routes stores hostname, backend
info, and cert paths for persistence across agent restarts.

ProxyRouter gains CertPath/KeyPath helpers for consistent cert path
construction.

Security:
- Backend hostname must resolve to a Tailnet IP (100.64.0.0/10)
- backend_tls=false is rejected (no cleartext to backends)
- Cert provisioning failure fails the setup (no route to missing cert)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:13:10 -07:00

160 lines
4.5 KiB
Go

package registry
import (
"database/sql"
"fmt"
_ "modernc.org/sqlite"
)
// Open opens the registry database at the given path and runs migrations.
func Open(path string) (*sql.DB, error) {
db, err := sql.Open("sqlite", path)
if err != nil {
return nil, fmt.Errorf("open database: %w", err)
}
for _, pragma := range []string{
"PRAGMA journal_mode = WAL",
"PRAGMA foreign_keys = ON",
"PRAGMA busy_timeout = 5000",
} {
if _, err := db.Exec(pragma); err != nil {
_ = db.Close()
return nil, fmt.Errorf("exec %q: %w", pragma, err)
}
}
if err := migrate(db); err != nil {
_ = db.Close()
return nil, fmt.Errorf("migrate: %w", err)
}
return db, nil
}
func migrate(db *sql.DB) error {
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`)
if err != nil {
return fmt.Errorf("create migrations table: %w", err)
}
for i, m := range migrations {
version := i + 1
var count int
if err := db.QueryRow("SELECT COUNT(*) FROM schema_migrations WHERE version = ?", version).Scan(&count); err != nil {
return fmt.Errorf("check migration %d: %w", version, err)
}
if count > 0 {
continue
}
if _, err := db.Exec(m); err != nil {
return fmt.Errorf("run migration %d: %w", version, err)
}
if _, err := db.Exec("INSERT INTO schema_migrations (version) VALUES (?)", version); err != nil {
return fmt.Errorf("record migration %d: %w", version, err)
}
}
return nil
}
var migrations = []string{
// Migration 1: initial schema
`
CREATE TABLE IF NOT EXISTS services (
name TEXT PRIMARY KEY,
active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS components (
name TEXT NOT NULL,
service TEXT NOT NULL REFERENCES services(name) ON DELETE CASCADE,
image TEXT NOT NULL,
network TEXT NOT NULL DEFAULT 'bridge',
user_spec TEXT NOT NULL DEFAULT '',
restart TEXT NOT NULL DEFAULT 'unless-stopped',
desired_state TEXT NOT NULL DEFAULT 'running',
observed_state TEXT NOT NULL DEFAULT 'unknown',
version TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (service, name)
);
CREATE TABLE IF NOT EXISTS component_ports (
service TEXT NOT NULL,
component TEXT NOT NULL,
mapping TEXT NOT NULL,
PRIMARY KEY (service, component, mapping),
FOREIGN KEY (service, component) REFERENCES components(service, name) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS component_volumes (
service TEXT NOT NULL,
component TEXT NOT NULL,
mapping TEXT NOT NULL,
PRIMARY KEY (service, component, mapping),
FOREIGN KEY (service, component) REFERENCES components(service, name) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS component_cmd (
service TEXT NOT NULL,
component TEXT NOT NULL,
position INTEGER NOT NULL,
arg TEXT NOT NULL,
PRIMARY KEY (service, component, position),
FOREIGN KEY (service, component) REFERENCES components(service, name) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
service TEXT NOT NULL,
component TEXT NOT NULL,
prev_state TEXT NOT NULL,
new_state TEXT NOT NULL,
timestamp TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_events_component_time
ON events(service, component, timestamp);
`,
// Migration 2: component routes
`
CREATE TABLE IF NOT EXISTS component_routes (
service TEXT NOT NULL,
component TEXT NOT NULL,
name TEXT NOT NULL,
port INTEGER NOT NULL,
mode TEXT NOT NULL DEFAULT 'l4',
hostname TEXT NOT NULL DEFAULT '',
host_port INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (service, component, name),
FOREIGN KEY (service, component) REFERENCES components(service, name) ON DELETE CASCADE
);
`,
// Migration 3: service comment
`ALTER TABLE services ADD COLUMN comment TEXT NOT NULL DEFAULT '';`,
// Migration 4: edge routes (v2 — public routes managed by the master)
`CREATE TABLE IF NOT EXISTS edge_routes (
hostname TEXT NOT NULL PRIMARY KEY,
backend_hostname TEXT NOT NULL,
backend_port INTEGER NOT NULL,
tls_cert TEXT NOT NULL DEFAULT '',
tls_key TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);`,
}