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>
100 lines
2.8 KiB
Go
100 lines
2.8 KiB
Go
package masterdb
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Placement records which node hosts which service.
|
|
type Placement struct {
|
|
ServiceName string
|
|
Node string
|
|
Tier string
|
|
DeployedAt time.Time
|
|
}
|
|
|
|
// CreatePlacement inserts or replaces a placement record.
|
|
func CreatePlacement(db *sql.DB, serviceName, node, tier string) error {
|
|
_, err := db.Exec(`
|
|
INSERT INTO placements (service_name, node, tier, deployed_at)
|
|
VALUES (?, ?, ?, datetime('now'))
|
|
ON CONFLICT(service_name) DO UPDATE SET
|
|
node = excluded.node,
|
|
tier = excluded.tier,
|
|
deployed_at = datetime('now')
|
|
`, serviceName, node, tier)
|
|
if err != nil {
|
|
return fmt.Errorf("create placement %s: %w", serviceName, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetPlacement returns the placement for a service.
|
|
func GetPlacement(db *sql.DB, serviceName string) (*Placement, error) {
|
|
var p Placement
|
|
var deployedAt string
|
|
err := db.QueryRow(`
|
|
SELECT service_name, node, tier, deployed_at
|
|
FROM placements WHERE service_name = ?
|
|
`, serviceName).Scan(&p.ServiceName, &p.Node, &p.Tier, &deployedAt)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get placement %s: %w", serviceName, err)
|
|
}
|
|
p.DeployedAt, _ = time.Parse("2006-01-02 15:04:05", deployedAt)
|
|
return &p, nil
|
|
}
|
|
|
|
// ListPlacements returns all placements.
|
|
func ListPlacements(db *sql.DB) ([]*Placement, error) {
|
|
rows, err := db.Query(`SELECT service_name, node, tier, deployed_at FROM placements ORDER BY service_name`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list placements: %w", err)
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
var placements []*Placement
|
|
for rows.Next() {
|
|
var p Placement
|
|
var deployedAt string
|
|
if err := rows.Scan(&p.ServiceName, &p.Node, &p.Tier, &deployedAt); err != nil {
|
|
return nil, fmt.Errorf("scan placement: %w", err)
|
|
}
|
|
p.DeployedAt, _ = time.Parse("2006-01-02 15:04:05", deployedAt)
|
|
placements = append(placements, &p)
|
|
}
|
|
return placements, rows.Err()
|
|
}
|
|
|
|
// DeletePlacement removes a placement record.
|
|
func DeletePlacement(db *sql.DB, serviceName string) error {
|
|
_, err := db.Exec(`DELETE FROM placements WHERE service_name = ?`, serviceName)
|
|
if err != nil {
|
|
return fmt.Errorf("delete placement %s: %w", serviceName, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CountPlacementsPerNode returns a map of node name → number of placed services.
|
|
func CountPlacementsPerNode(db *sql.DB) (map[string]int, error) {
|
|
rows, err := db.Query(`SELECT node, COUNT(*) FROM placements GROUP BY node`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("count placements: %w", err)
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
counts := make(map[string]int)
|
|
for rows.Next() {
|
|
var node string
|
|
var count int
|
|
if err := rows.Scan(&node, &count); err != nil {
|
|
return nil, fmt.Errorf("scan count: %w", err)
|
|
}
|
|
counts[node] = count
|
|
}
|
|
return counts, rows.Err()
|
|
}
|