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:
@@ -21,16 +21,27 @@ type ServiceDef struct {
|
||||
Components []ComponentDef `toml:"components"`
|
||||
}
|
||||
|
||||
// RouteDef describes a route for a component, used for automatic port
|
||||
// allocation and mc-proxy integration.
|
||||
type RouteDef struct {
|
||||
Name string `toml:"name,omitempty"`
|
||||
Port int `toml:"port"`
|
||||
Mode string `toml:"mode,omitempty"`
|
||||
Hostname string `toml:"hostname,omitempty"`
|
||||
}
|
||||
|
||||
// ComponentDef describes a single container component within a service.
|
||||
type ComponentDef struct {
|
||||
Name string `toml:"name"`
|
||||
Image string `toml:"image"`
|
||||
Network string `toml:"network,omitempty"`
|
||||
User string `toml:"user,omitempty"`
|
||||
Restart string `toml:"restart,omitempty"`
|
||||
Ports []string `toml:"ports,omitempty"`
|
||||
Volumes []string `toml:"volumes,omitempty"`
|
||||
Cmd []string `toml:"cmd,omitempty"`
|
||||
Name string `toml:"name"`
|
||||
Image string `toml:"image"`
|
||||
Network string `toml:"network,omitempty"`
|
||||
User string `toml:"user,omitempty"`
|
||||
Restart string `toml:"restart,omitempty"`
|
||||
Ports []string `toml:"ports,omitempty"`
|
||||
Volumes []string `toml:"volumes,omitempty"`
|
||||
Cmd []string `toml:"cmd,omitempty"`
|
||||
Routes []RouteDef `toml:"routes,omitempty"`
|
||||
Env []string `toml:"env,omitempty"`
|
||||
}
|
||||
|
||||
// Load reads and parses a TOML service definition file. If the active field
|
||||
@@ -129,11 +140,46 @@ func validate(def *ServiceDef) error {
|
||||
return fmt.Errorf("duplicate component name %q in service %q", c.Name, def.Name)
|
||||
}
|
||||
seen[c.Name] = true
|
||||
|
||||
if err := validateRoutes(c.Name, def.Name, c.Routes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateRoutes checks that routes within a component are valid.
|
||||
func validateRoutes(compName, svcName string, routes []RouteDef) error {
|
||||
if len(routes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
routeNames := make(map[string]bool)
|
||||
for i, r := range routes {
|
||||
if r.Port <= 0 {
|
||||
return fmt.Errorf("route port must be > 0 in component %q of service %q", compName, svcName)
|
||||
}
|
||||
if r.Mode != "" && r.Mode != "l4" && r.Mode != "l7" {
|
||||
return fmt.Errorf("route mode must be \"l4\" or \"l7\" in component %q of service %q", compName, svcName)
|
||||
}
|
||||
if len(routes) > 1 && r.Name == "" {
|
||||
return fmt.Errorf("route name is required when component has multiple routes in component %q of service %q", compName, svcName)
|
||||
}
|
||||
|
||||
// Use index-based key for unnamed single routes.
|
||||
key := r.Name
|
||||
if key == "" {
|
||||
key = fmt.Sprintf("_route_%d", i)
|
||||
}
|
||||
if routeNames[key] {
|
||||
return fmt.Errorf("duplicate route name %q in component %q of service %q", r.Name, compName, svcName)
|
||||
}
|
||||
routeNames[key] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToProto converts a ServiceDef to a proto ServiceSpec.
|
||||
func ToProto(def *ServiceDef) *mcpv1.ServiceSpec {
|
||||
spec := &mcpv1.ServiceSpec{
|
||||
@@ -142,7 +188,7 @@ func ToProto(def *ServiceDef) *mcpv1.ServiceSpec {
|
||||
}
|
||||
|
||||
for _, c := range def.Components {
|
||||
spec.Components = append(spec.Components, &mcpv1.ComponentSpec{
|
||||
cs := &mcpv1.ComponentSpec{
|
||||
Name: c.Name,
|
||||
Image: c.Image,
|
||||
Network: c.Network,
|
||||
@@ -151,7 +197,17 @@ func ToProto(def *ServiceDef) *mcpv1.ServiceSpec {
|
||||
Ports: c.Ports,
|
||||
Volumes: c.Volumes,
|
||||
Cmd: c.Cmd,
|
||||
})
|
||||
Env: c.Env,
|
||||
}
|
||||
for _, r := range c.Routes {
|
||||
cs.Routes = append(cs.Routes, &mcpv1.RouteSpec{
|
||||
Name: r.Name,
|
||||
Port: int32(r.Port),
|
||||
Mode: r.Mode,
|
||||
Hostname: r.Hostname,
|
||||
})
|
||||
}
|
||||
spec.Components = append(spec.Components, cs)
|
||||
}
|
||||
|
||||
return spec
|
||||
@@ -169,7 +225,7 @@ func FromProto(spec *mcpv1.ServiceSpec, node string) *ServiceDef {
|
||||
}
|
||||
|
||||
for _, c := range spec.GetComponents() {
|
||||
def.Components = append(def.Components, ComponentDef{
|
||||
cd := ComponentDef{
|
||||
Name: c.GetName(),
|
||||
Image: c.GetImage(),
|
||||
Network: c.GetNetwork(),
|
||||
@@ -178,7 +234,17 @@ func FromProto(spec *mcpv1.ServiceSpec, node string) *ServiceDef {
|
||||
Ports: c.GetPorts(),
|
||||
Volumes: c.GetVolumes(),
|
||||
Cmd: c.GetCmd(),
|
||||
})
|
||||
Env: c.GetEnv(),
|
||||
}
|
||||
for _, r := range c.GetRoutes() {
|
||||
cd.Routes = append(cd.Routes, RouteDef{
|
||||
Name: r.GetName(),
|
||||
Port: int(r.GetPort()),
|
||||
Mode: r.GetMode(),
|
||||
Hostname: r.GetHostname(),
|
||||
})
|
||||
}
|
||||
def.Components = append(def.Components, cd)
|
||||
}
|
||||
|
||||
return def
|
||||
|
||||
Reference in New Issue
Block a user