Everything downstream reads the port: the provider is told where to reach the consumer, and the redirection that turns a declared port into the number the machine published is keyed on it. A route naming only its endpoint left the proxy with no port at all, and a proxy with no port has nothing to dial. Caught after the catalogue had already been changed to name endpoints and before the mesh picked those manifests up, which is the only reason nothing broke: every module's manifest is behind its source right now, so the plan still renders from the old shape. The declared port, not the machine one — the redirection happens later and is keyed on the declared number, so filling in the machine port here would be redirected twice or not at all. A route that repeats a port keeps it.
206 lines
7.9 KiB
Go
206 lines
7.9 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// aMediaServer is the shape one port number per key cannot express: two endpoints of different kinds.
|
|
// A web surface a proxy serves under a subdomain, and a protocol port clients dial directly because
|
|
// the client expects that number.
|
|
func aMediaServer() Manifest {
|
|
return Manifest{
|
|
Module: "media",
|
|
Listens: []Listening{
|
|
{Name: "web", Port: 80, From: FromMesh, Why: "the app, behind the proxy"},
|
|
{Name: "stream", Port: 32400, From: FromEverywhere, Fixed: true,
|
|
Why: "the client dials this number; the protocol chose it"},
|
|
},
|
|
Contributes: map[string]map[string]any{
|
|
"route": {"label": "media", RouteEndpoint: "web"},
|
|
},
|
|
// Both endpoints are published by its container, which is what lets a machine port be given
|
|
// for either: the mesh moves a port the module publishes, never one it merely listens on.
|
|
Resources: []map[string]any{
|
|
{"id": "server", "type": "container", "name": "media",
|
|
"ports": []any{"80", "32400"}},
|
|
},
|
|
}
|
|
}
|
|
|
|
// **A route names the endpoint it serves.** A route and a listen both carried a port and nothing said
|
|
// they were the same thing; now one of them says so.
|
|
func TestARouteNamesTheEndpointItServes(t *testing.T) {
|
|
m := aMediaServer()
|
|
if port, ok := EndpointPort(m, "web"); !ok || port != 80 {
|
|
t.Fatalf("the web endpoint resolves to %d (%v), want 80", port, ok)
|
|
}
|
|
if port, ok := EndpointPort(m, "stream"); !ok || port != 32400 {
|
|
t.Fatalf("the stream endpoint resolves to %d (%v), want 32400", port, ok)
|
|
}
|
|
if _, ok := EndpointPort(m, "absent"); ok {
|
|
t.Fatal("an endpoint the module does not declare resolved to a port")
|
|
}
|
|
}
|
|
|
|
// And the routed set is read through the name, so the endpoint the proxy serves is known without a
|
|
// reader joining two numbers.
|
|
func TestTheRoutedEndpointIsFoundByName(t *testing.T) {
|
|
routed := RoutedPorts(aMediaServer())
|
|
if !routed[80] {
|
|
t.Fatalf("the routed endpoint was not found by name: %v", routed)
|
|
}
|
|
// And the directly-dialled one is not routed, which is what lets its reach govern its port.
|
|
if routed[32400] {
|
|
t.Fatalf("the endpoint clients dial directly reads as routed: %v", routed)
|
|
}
|
|
}
|
|
|
|
// **Two endpoints of different shapes, configured as themselves.** The web endpoint's reach asks for
|
|
// names and leaves its port to the proxy; the stream endpoint's reach governs its port, because
|
|
// clients dial it and there is no name.
|
|
func TestTwoEndpointsOfDifferentShapesAreConfiguredSeparately(t *testing.T) {
|
|
m := aMediaServer()
|
|
settings := SettingsBy{"media": {{From: "node anchor", Values: map[string]any{
|
|
ReachSetting: map[string]any{"80": ReachBoth, "32400": ReachPublic},
|
|
}}}}
|
|
|
|
r := Resolution{Node: "anchor", Modules: []Manifest{m},
|
|
PublicDomain: "example.test", At: "anchor.internal"}
|
|
rules, err := r.Rules(Rendering{Settings: settings})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, rule := range rules {
|
|
switch rule.Port {
|
|
case 80:
|
|
if rule.From != FromMesh {
|
|
t.Fatalf("the routed endpoint's port opened to %q; the proxy is how it is reached",
|
|
rule.From)
|
|
}
|
|
case 32400:
|
|
if rule.From != FromEverywhere {
|
|
t.Fatalf("the directly-dialled endpoint's port is %q, want anywhere", rule.From)
|
|
}
|
|
}
|
|
}
|
|
|
|
// And the routed one carries both names, asked for by the same statement.
|
|
given, err := r.contributions(settings, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var public, internal string
|
|
for _, c := range given["route"] {
|
|
public, _ = c.Values["name"].(string)
|
|
internal, _ = c.Values["internal-name"].(string)
|
|
}
|
|
if public != "media.example.test" || internal != "media.anchor.internal" {
|
|
t.Fatalf("names are %q and %q, want both", public, internal)
|
|
}
|
|
}
|
|
|
|
// A route naming an endpoint the module does not declare reaches nothing, and is refused where it is
|
|
// written rather than resolving to no port and serving nothing.
|
|
func TestARouteNamingAnEndpointTheModuleLacksIsRefused(t *testing.T) {
|
|
m := aMediaServer()
|
|
m.Contributes["route"][RouteEndpoint] = "absent"
|
|
got := strings.Join(RouteProblems(m), "\n")
|
|
if !strings.Contains(got, "does not declare") {
|
|
t.Fatalf("a route naming an absent endpoint was accepted:\n%s", got)
|
|
}
|
|
}
|
|
|
|
// **Two endpoints called the same would make an assignment configure whichever was read last.** The
|
|
// point of a name is that it identifies one thing.
|
|
func TestTwoEndpointsWithOneNameAreRefused(t *testing.T) {
|
|
m := Manifest{Module: "twice", Listens: []Listening{
|
|
{Name: "web", Port: 80, From: FromMesh},
|
|
{Name: "web", Port: 8080, From: FromMesh},
|
|
}}
|
|
got := strings.Join(endpointNameProblems(m), "\n")
|
|
if !strings.Contains(got, "could mean either") {
|
|
t.Fatalf("two endpoints with one name were accepted:\n%s", got)
|
|
}
|
|
}
|
|
|
|
// A name that is not a name is refused where it is written: it ends up in something a person types.
|
|
func TestAnEndpointNameIsHeldToItsShape(t *testing.T) {
|
|
for _, wrong := range []string{"Web", "web port", "3000", "-web", "web_surface"} {
|
|
m := Manifest{Module: "odd", Listens: []Listening{{Name: wrong, Port: 80, From: FromMesh}}}
|
|
if got := strings.Join(endpointNameProblems(m), "\n"); !strings.Contains(got, "a name is lowercase") {
|
|
t.Fatalf("%q was accepted as an endpoint name:\n%s", wrong, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **Every endpoint in the catalogue is unnamed today, and must stay valid.** The word ships one
|
|
// release before anything uses it.
|
|
func TestAnUnnamedEndpointIsStillValid(t *testing.T) {
|
|
m := Manifest{Module: "ordinary", Listens: []Listening{{Port: 443, From: FromEverywhere}}}
|
|
if got := endpointNameProblems(m); len(got) != 0 {
|
|
t.Fatalf("an unnamed endpoint was refused: %v", got)
|
|
}
|
|
if got := RouteProblems(m); len(got) != 0 {
|
|
t.Fatalf("a module with no route was refused: %v", got)
|
|
}
|
|
}
|
|
|
|
// **A route that names an endpoint still carries that endpoint's port.**
|
|
//
|
|
// Everything downstream reads the port: the provider is told where to reach the consumer, and the
|
|
// redirection that turns a declared port into the number the machine published is keyed on it. A route
|
|
// naming only its endpoint left the proxy with no port, and a proxy with no port has nothing to dial.
|
|
//
|
|
// Caught after the catalogue had already been changed to name endpoints, and before the mesh picked
|
|
// those manifests up — which is the only reason nothing broke.
|
|
func TestARouteNamingAnEndpointStillCarriesItsPort(t *testing.T) {
|
|
m := aMediaServer()
|
|
r := Resolution{Node: "anchor", Modules: []Manifest{m},
|
|
PublicDomain: "example.test", At: "anchor.internal"}
|
|
given, err := r.contributions(nil, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var saw bool
|
|
for _, c := range given["route"] {
|
|
saw = true
|
|
port, ok := asPort(c.Values["port"])
|
|
if !ok {
|
|
t.Fatalf("the route carries no port, so the proxy has nothing to dial: %v", c.Values)
|
|
}
|
|
if port != 80 {
|
|
t.Fatalf("the route carries port %d, want the web endpoint's 80", port)
|
|
}
|
|
}
|
|
if !saw {
|
|
t.Fatal("the module contributed no route")
|
|
}
|
|
}
|
|
|
|
// And the declared port, not the machine one: the redirection to where the machine published it
|
|
// happens later and is keyed on the declared number, so filling the machine port in here would be
|
|
// redirected twice or not at all.
|
|
func TestTheEndpointsDeclaredPortIsFilledInNotTheMachineOne(t *testing.T) {
|
|
m := aMediaServer()
|
|
values := map[string]any{RouteEndpoint: "web", "label": "media"}
|
|
portOfEndpoint(values, endpointPorts(m))
|
|
if got, _ := asPort(values["port"]); got != 80 {
|
|
t.Fatalf("filled in port %d, want the declared 80", got)
|
|
}
|
|
// Then the ordinary redirection puts it where the machine published it.
|
|
moved := atMachinePort(values, m.Module, map[string]map[int]int{"media": {80: 20009}})
|
|
if got, _ := asPort(moved["port"]); got != 20009 {
|
|
t.Fatalf("after redirection the port is %d, want the machine's 20009", got)
|
|
}
|
|
}
|
|
|
|
// A route that repeats a port keeps it, because that is the older shape and still read.
|
|
func TestARouteThatRepeatsItsPortKeepsIt(t *testing.T) {
|
|
values := map[string]any{RouteEndpoint: "web", "port": 8080}
|
|
portOfEndpoint(values, map[string]int{"web": 80})
|
|
if got, _ := asPort(values["port"]); got != 8080 {
|
|
t.Fatalf("the port it stated was overwritten with %d", got)
|
|
}
|
|
}
|