Files
mcp/internal/masterdb/db.go
Kyle Isom 3c0b55f9f8 Add master database with nodes, placements, and edge_routes
New internal/masterdb/ package for mcp-master cluster state. Separate
from the agent's registry because the schemas are fundamentally
different (cluster-wide placement vs node-local containers).

Tables: nodes, placements, edge_routes. Full CRUD with tests.
Follows the same Open/migrate pattern as internal/registry/.

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

107 lines
2.9 KiB
Go

// Package masterdb provides the SQLite database for the mcp-master daemon.
// It stores the cluster-wide node registry, service placements, and edge routes.
// This is separate from the agent's registry (internal/registry/) because the
// master and agent have fundamentally different schemas.
package masterdb
import (
"database/sql"
"fmt"
_ "modernc.org/sqlite"
)
// Open opens the master 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: cluster state
`
CREATE TABLE IF NOT EXISTS nodes (
name TEXT PRIMARY KEY,
address TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'worker',
arch TEXT NOT NULL DEFAULT 'amd64',
status TEXT NOT NULL DEFAULT 'unknown',
containers INTEGER NOT NULL DEFAULT 0,
last_heartbeat TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS placements (
service_name TEXT PRIMARY KEY,
node TEXT NOT NULL REFERENCES nodes(name),
tier TEXT NOT NULL DEFAULT 'worker',
deployed_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS edge_routes (
hostname TEXT PRIMARY KEY,
service_name TEXT NOT NULL,
edge_node TEXT NOT NULL REFERENCES nodes(name),
backend_hostname TEXT NOT NULL,
backend_port INTEGER NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_edge_routes_service
ON edge_routes(service_name);
`,
}