diff --git a/internal/catalogue/declaration.go b/internal/catalogue/declaration.go index 5af2bb8..e361d84 100644 --- a/internal/catalogue/declaration.go +++ b/internal/catalogue/declaration.go @@ -1576,14 +1576,23 @@ func publishedOn(resource map[string]any, module string, with Rendering) { out = append(out, givenOuter(written, with.Given[module])) continue } - wanted, err := strconv.Atoi(strings.TrimSpace(written)) + // A short form may carry the protocol — `"3478/udp"` — and the number is what the mesh + // assigns for; the protocol rides along. Read as one token, the `/udp` made the whole + // entry "not a port", and passing it through let the runtime publish it wherever it + // liked: unifi's STUN and discovery landed on random machine ports while every TCP pin + // beside them held. + mapping, protocol := written, "" + if cut := strings.LastIndex(written, "/"); cut >= 0 { + mapping, protocol = written[:cut], written[cut:] + } + wanted, err := strconv.Atoi(strings.TrimSpace(mapping)) if err != nil { // Not a port at all. Passed through, so the host refuses it with its own words rather // than this quietly dropping something somebody meant. out = append(out, written) continue } - out = append(out, fmt.Sprintf("%d:%d", with.machinePort(module, wanted), wanted)) + out = append(out, fmt.Sprintf("%d:%d%s", with.machinePort(module, wanted), wanted, protocol)) } resource["ports"] = out } diff --git a/internal/catalogue/short_form_port_protocol_test.go b/internal/catalogue/short_form_port_protocol_test.go new file mode 100644 index 0000000..625227e --- /dev/null +++ b/internal/catalogue/short_form_port_protocol_test.go @@ -0,0 +1,24 @@ +package catalogue + +import ( + "fmt" + "testing" +) + +// A short-form port may name its protocol — "3478/udp" — and the mesh assigns the number exactly +// as it does for "3478": the protocol rides along on the outside. Read as one token, the suffix made +// the entry "not a port" and the runtime published it on a random machine port (found on ace: unifi's +// STUN and discovery, while every TCP pin beside them held). +func TestAShortFormPortKeepsItsProtocolAndGetsItsMachinePort(t *testing.T) { + container := map[string]any{"type": "container", "ports": []any{"3478/udp", "8443", "10001/udp"}} + with := Rendering{ + Given: map[string]map[int]int{"unifi": {3478: 3478}}, + Ports: map[string]map[int]int{"unifi": {8443: 20010, 10001: 20011}}, + } + publishedOn(container, "unifi", with) + got := fmt.Sprint(container["ports"]) + want := "[3478:3478/udp 20010:8443 20011:10001/udp]" + if got != want { + t.Fatalf("published %s, want %s", got, want) + } +}