Add route declarations and automatic port allocation to MCP agent
Service definitions can now declare routes per component instead of manual port mappings: [[components.routes]] name = "rest" port = 8443 mode = "l4" The agent allocates free host ports at deploy time and injects $PORT/$PORT_<NAME> env vars into containers. Backward compatible: components with old-style ports= work unchanged. Changes: - Proto: RouteSpec message, routes + env fields on ComponentSpec - Servicedef: RouteDef parsing and validation from TOML - Registry: component_routes table with host_port tracking - Runtime: Env field on ContainerSpec, -e flag in BuildRunArgs - Agent: PortAllocator (random 10000-60000, availability check), deploy wiring for route→port mapping and env injection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,15 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Route represents a route entry for a component in the registry.
|
||||
type Route struct {
|
||||
Name string
|
||||
Port int
|
||||
Mode string
|
||||
Hostname string
|
||||
HostPort int // agent-assigned host port (0 = not yet allocated)
|
||||
}
|
||||
|
||||
// Component represents a component in the registry.
|
||||
type Component struct {
|
||||
Name string
|
||||
@@ -20,6 +29,7 @@ type Component struct {
|
||||
Ports []string
|
||||
Volumes []string
|
||||
Cmd []string
|
||||
Routes []Route
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
@@ -51,6 +61,9 @@ func CreateComponent(db *sql.DB, c *Component) error {
|
||||
if err := setCmd(tx, c.Service, c.Name, c.Cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := setRoutes(tx, c.Service, c.Name, c.Routes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
@@ -84,6 +97,10 @@ func GetComponent(db *sql.DB, service, name string) (*Component, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Routes, err = getRoutes(db, service, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
@@ -115,6 +132,7 @@ func ListComponents(db *sql.DB, service string) ([]Component, error) {
|
||||
c.Ports, _ = getPorts(db, c.Service, c.Name)
|
||||
c.Volumes, _ = getVolumes(db, c.Service, c.Name)
|
||||
c.Cmd, _ = getCmd(db, c.Service, c.Name)
|
||||
c.Routes, _ = getRoutes(db, c.Service, c.Name)
|
||||
|
||||
components = append(components, c)
|
||||
}
|
||||
@@ -168,6 +186,9 @@ func UpdateComponentSpec(db *sql.DB, c *Component) error {
|
||||
if err := setCmd(tx, c.Service, c.Name, c.Cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := setRoutes(tx, c.Service, c.Name, c.Routes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
@@ -274,3 +295,85 @@ func getCmd(db *sql.DB, service, component string) ([]string, error) {
|
||||
}
|
||||
return cmd, rows.Err()
|
||||
}
|
||||
|
||||
// helper: set route definitions (delete + re-insert)
|
||||
func setRoutes(tx *sql.Tx, service, component string, routes []Route) error {
|
||||
if _, err := tx.Exec("DELETE FROM component_routes WHERE service = ? AND component = ?", service, component); err != nil {
|
||||
return fmt.Errorf("clear routes %q/%q: %w", service, component, err)
|
||||
}
|
||||
for _, r := range routes {
|
||||
mode := r.Mode
|
||||
if mode == "" {
|
||||
mode = "l4"
|
||||
}
|
||||
name := r.Name
|
||||
if name == "" {
|
||||
name = "default"
|
||||
}
|
||||
if _, err := tx.Exec(
|
||||
"INSERT INTO component_routes (service, component, name, port, mode, hostname, host_port) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
service, component, name, r.Port, mode, r.Hostname, r.HostPort,
|
||||
); err != nil {
|
||||
return fmt.Errorf("insert route %q/%q %q: %w", service, component, name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRoutes(db *sql.DB, service, component string) ([]Route, error) {
|
||||
rows, err := db.Query(
|
||||
"SELECT name, port, mode, hostname, host_port FROM component_routes WHERE service = ? AND component = ? ORDER BY name",
|
||||
service, component,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get routes %q/%q: %w", service, component, err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
var routes []Route
|
||||
for rows.Next() {
|
||||
var r Route
|
||||
if err := rows.Scan(&r.Name, &r.Port, &r.Mode, &r.Hostname, &r.HostPort); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
routes = append(routes, r)
|
||||
}
|
||||
return routes, rows.Err()
|
||||
}
|
||||
|
||||
// UpdateRouteHostPort updates the agent-assigned host port for a specific route.
|
||||
func UpdateRouteHostPort(db *sql.DB, service, component, routeName string, hostPort int) error {
|
||||
res, err := db.Exec(
|
||||
"UPDATE component_routes SET host_port = ? WHERE service = ? AND component = ? AND name = ?",
|
||||
hostPort, service, component, routeName,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update route host_port %q/%q/%q: %w", service, component, routeName, err)
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
return fmt.Errorf("update route host_port %q/%q/%q: %w", service, component, routeName, sql.ErrNoRows)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRouteHostPorts returns a map of route name to assigned host port for a component.
|
||||
func GetRouteHostPorts(db *sql.DB, service, component string) (map[string]int, error) {
|
||||
rows, err := db.Query(
|
||||
"SELECT name, host_port FROM component_routes WHERE service = ? AND component = ?",
|
||||
service, component,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get route host ports %q/%q: %w", service, component, err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
result := make(map[string]int)
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var port int
|
||||
if err := rows.Scan(&name, &port); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[name] = port
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user