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:
69
internal/agent/portalloc.go
Normal file
69
internal/agent/portalloc.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
portRangeMin = 10000
|
||||
portRangeMax = 60000
|
||||
maxRetries = 10
|
||||
)
|
||||
|
||||
// PortAllocator manages host port allocation for route-based deployments.
|
||||
// It tracks allocated ports within the agent session to avoid double-allocation.
|
||||
type PortAllocator struct {
|
||||
mu sync.Mutex
|
||||
allocated map[int]bool
|
||||
}
|
||||
|
||||
// NewPortAllocator creates a new PortAllocator.
|
||||
func NewPortAllocator() *PortAllocator {
|
||||
return &PortAllocator{
|
||||
allocated: make(map[int]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate picks a free port in range [10000, 60000).
|
||||
// It tries random ports, checks availability with net.Listen, and retries up to 10 times.
|
||||
func (pa *PortAllocator) Allocate() (int, error) {
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
|
||||
for i := range maxRetries {
|
||||
port := portRangeMin + rand.IntN(portRangeMax-portRangeMin)
|
||||
if pa.allocated[port] {
|
||||
continue
|
||||
}
|
||||
|
||||
if !isPortFree(port) {
|
||||
continue
|
||||
}
|
||||
|
||||
pa.allocated[port] = true
|
||||
return port, nil
|
||||
_ = i
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("failed to allocate port after %d attempts", maxRetries)
|
||||
}
|
||||
|
||||
// Release marks a port as available again.
|
||||
func (pa *PortAllocator) Release(port int) {
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
delete(pa.allocated, port)
|
||||
}
|
||||
|
||||
// isPortFree checks if a TCP port is available by attempting to listen on it.
|
||||
func isPortFree(port int) bool {
|
||||
ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_ = ln.Close()
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user