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>
146 lines
3.6 KiB
Go
146 lines
3.6 KiB
Go
package runtime
|
|
|
|
import (
|
|
"slices"
|
|
"testing"
|
|
)
|
|
|
|
func requireEqualArgs(t *testing.T, got, want []string) {
|
|
t.Helper()
|
|
if !slices.Equal(got, want) {
|
|
t.Fatalf("args mismatch\ngot: %v\nwant: %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildRunArgs(t *testing.T) {
|
|
p := &Podman{}
|
|
|
|
t.Run("full spec", func(t *testing.T) {
|
|
spec := ContainerSpec{
|
|
Name: "metacrypt-api",
|
|
Image: "mcr.svc.mcp.metacircular.net:8443/metacrypt:v1.0.0",
|
|
Network: "docker_default",
|
|
User: "0:0",
|
|
Restart: "unless-stopped",
|
|
Ports: []string{"127.0.0.1:18443:8443", "127.0.0.1:19443:9443"},
|
|
Volumes: []string{"/srv/metacrypt:/srv/metacrypt", "/etc/ssl:/etc/ssl:ro"},
|
|
Cmd: []string{"server", "--config", "/srv/metacrypt/metacrypt.toml"},
|
|
}
|
|
requireEqualArgs(t, p.BuildRunArgs(spec), []string{
|
|
"run", "-d", "--name", "metacrypt-api",
|
|
"--network", "docker_default",
|
|
"--user", "0:0",
|
|
"--restart", "unless-stopped",
|
|
"-p", "127.0.0.1:18443:8443",
|
|
"-p", "127.0.0.1:19443:9443",
|
|
"-v", "/srv/metacrypt:/srv/metacrypt",
|
|
"-v", "/etc/ssl:/etc/ssl:ro",
|
|
"mcr.svc.mcp.metacircular.net:8443/metacrypt:v1.0.0",
|
|
"server", "--config", "/srv/metacrypt/metacrypt.toml",
|
|
})
|
|
})
|
|
|
|
t.Run("minimal spec", func(t *testing.T) {
|
|
spec := ContainerSpec{
|
|
Name: "test-app",
|
|
Image: "img:latest",
|
|
}
|
|
requireEqualArgs(t, p.BuildRunArgs(spec), []string{
|
|
"run", "-d", "--name", "test-app", "img:latest",
|
|
})
|
|
})
|
|
|
|
t.Run("ports only", func(t *testing.T) {
|
|
spec := ContainerSpec{
|
|
Name: "test-app",
|
|
Image: "img:latest",
|
|
Ports: []string{"8080:80", "8443:443"},
|
|
}
|
|
requireEqualArgs(t, p.BuildRunArgs(spec), []string{
|
|
"run", "-d", "--name", "test-app",
|
|
"-p", "8080:80", "-p", "8443:443",
|
|
"img:latest",
|
|
})
|
|
})
|
|
|
|
t.Run("volumes only", func(t *testing.T) {
|
|
spec := ContainerSpec{
|
|
Name: "test-app",
|
|
Image: "img:latest",
|
|
Volumes: []string{"/data:/data", "/config:/config:ro"},
|
|
}
|
|
requireEqualArgs(t, p.BuildRunArgs(spec), []string{
|
|
"run", "-d", "--name", "test-app",
|
|
"-v", "/data:/data", "-v", "/config:/config:ro",
|
|
"img:latest",
|
|
})
|
|
})
|
|
|
|
t.Run("env vars", func(t *testing.T) {
|
|
spec := ContainerSpec{
|
|
Name: "test-app",
|
|
Image: "img:latest",
|
|
Env: []string{"PORT=12345", "PORT_GRPC=12346"},
|
|
}
|
|
requireEqualArgs(t, p.BuildRunArgs(spec), []string{
|
|
"run", "-d", "--name", "test-app",
|
|
"-e", "PORT=12345", "-e", "PORT_GRPC=12346",
|
|
"img:latest",
|
|
})
|
|
})
|
|
|
|
t.Run("full spec with env", func(t *testing.T) {
|
|
spec := ContainerSpec{
|
|
Name: "svc-api",
|
|
Image: "img:latest",
|
|
Network: "net",
|
|
Ports: []string{"127.0.0.1:12345:8443"},
|
|
Volumes: []string{"/srv:/srv"},
|
|
Env: []string{"PORT=12345"},
|
|
}
|
|
requireEqualArgs(t, p.BuildRunArgs(spec), []string{
|
|
"run", "-d", "--name", "svc-api",
|
|
"--network", "net",
|
|
"-p", "127.0.0.1:12345:8443",
|
|
"-v", "/srv:/srv",
|
|
"-e", "PORT=12345",
|
|
"img:latest",
|
|
})
|
|
})
|
|
|
|
t.Run("cmd after image", func(t *testing.T) {
|
|
spec := ContainerSpec{
|
|
Name: "test-app",
|
|
Image: "img:latest",
|
|
Cmd: []string{"serve", "--port", "8080"},
|
|
}
|
|
requireEqualArgs(t, p.BuildRunArgs(spec), []string{
|
|
"run", "-d", "--name", "test-app",
|
|
"img:latest",
|
|
"serve", "--port", "8080",
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestExtractVersion(t *testing.T) {
|
|
tests := []struct {
|
|
image string
|
|
want string
|
|
}{
|
|
{"registry.example.com:5000/img:v1.2.0", "v1.2.0"},
|
|
{"img:latest", "latest"},
|
|
{"img", ""},
|
|
{"registry.example.com/path/img:v1", "v1"},
|
|
{"registry.example.com:5000/path/img", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.image, func(t *testing.T) {
|
|
got := ExtractVersion(tt.image)
|
|
if got != tt.want {
|
|
t.Fatalf("ExtractVersion(%q) = %q, want %q", tt.image, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|