package catalogue import ( "encoding/json" "reflect" "slices" "strings" "testing" ) // novox/hq ADR 0100: on an adopted node the found firewall stays in force. The mesh declares // openings where it would have loaded a filter, and guards its own ports in a table that only // refuses. // hub is the private network's generator on the hub: it opens the hub's port from anywhere. type hub struct{} func (hub) Resources(string) ([]map[string]any, bool, error) { return []map[string]any{{"id": "config", "type": "file", "path": "/etc/wireguard/mesh0.conf", "content": "[Interface]\n"}}, true, nil } func (hub) Listens(string) ([]Listening, error) { return []Listening{{Port: 51820, Protocol: "udp", From: FromEverywhere}}, nil } // anAdoptedAnchor is the control-node's set: the store, the bus, the registry, the private network, // a served module and the filter module. func anAdoptedAnchor() Resolution { return Resolution{Node: "anchor", Modules: []Manifest{ {Module: "network", Computed: "overlay"}, {Module: "postgres", Guards: []int{5432}, Listens: []Listening{{Port: 5432, From: FromMesh}}, Resources: []map[string]any{{"id": "server", "type": "container", "name": "mesh-store", "ports": []any{"5432:5432"}}}}, {Module: "lavinmq", Guards: []int{15672}, Listens: []Listening{{Port: 5671, From: FromMesh}, {Port: 5672, From: FromMesh}}, Resources: []map[string]any{{"id": "server", "type": "container", "name": "mesh-broker", "ports": []any{"5671:5671", "5672:5672", "127.0.0.1:15672:15672"}}}}, {Module: "distribution", Listens: []Listening{{Port: 5000, From: FromMesh}}, Resources: []map[string]any{{"id": "store", "type": "container", "name": "registry", "ports": []any{"5000"}}}}, {Module: "hello-web", Listens: []Listening{{Port: 8080, From: FromEverywhere}}, Resources: []map[string]any{{"id": "server", "type": "container", "name": "hello-web", "ports": []any{"8080"}}}}, {Module: "helper", Listens: []Listening{{Port: 9000, From: FromMachine}}}, {Module: "nftables", Filtering: &Filtering{Into: "/etc/nftables.conf"}, Resources: []map[string]any{{"id": "load", "type": "service", "unit": "mesh-filter.service", "state": "running", "restart-on": []any{"filtering"}}}}, }} } func anchorRendering(adopted bool) Rendering { return Rendering{ Generators: map[string]Generator{"overlay": hub{}}, Ports: map[string]map[int]int{"distribution": {5000: 5000}, "hello-web": {8080: 20001}}, Settings: SettingsBy{"distribution": {{From: "node anchor", Values: map[string]any{ExposeSetting: map[string]any{"5000": FromEverywhere}}}}}, Mesh: []string{"10.42.0.1"}, Foundation: []int{5671}, Adopted: adopted, // Genesis takes the foundation's modules. Taken: map[string]bool{"postgres": true, "lavinmq": true}, } } func byID(resources []map[string]any) map[string]map[string]any { out := map[string]map[string]any{} for _, r := range resources { out[r["id"].(string)] = r } return out } func TestAnAdoptedNodeIsDeclaredOpeningsFromTheSameInputsAsTheFilter(t *testing.T) { composed, err := anAdoptedAnchor().Compose(anchorRendering(true)) if err != nil { t.Fatal(err) } got := byID(composed.Resources) want := map[string]map[string]any{ // The hub's port, from anywhere, received. "adoption.opening-udp-51820-incoming": {"port": 51820, "protocol": "udp", "from": "everywhere", "path": "incoming"}, // The store's port from the private network only, and forwarded: a container publishes it. "adoption.opening-tcp-5432-forwarded": {"port": 5432, "protocol": "tcp", "from": "mesh", "path": "forwarded", "to": 5432}, // The bus from anywhere: a node enrols over it before it has a private address. "adoption.opening-tcp-5671-forwarded": {"port": 5671, "protocol": "tcp", "from": "everywhere", "path": "forwarded", "to": 5671}, "adoption.opening-tcp-5672-forwarded": {"port": 5672, "protocol": "tcp", "from": "mesh", "path": "forwarded", "to": 5672}, // The registry from anywhere, by its node's exposure setting. "adoption.opening-tcp-5000-forwarded": {"port": 5000, "protocol": "tcp", "from": "everywhere", "path": "forwarded", "to": 5000}, // A published port names the machine port and the container port it is forwarded to. "adoption.opening-tcp-20001-forwarded": {"port": 20001, "protocol": "tcp", "from": "everywhere", "path": "forwarded", "to": 8080}, } for id, fields := range want { opening, ok := got[id] if !ok { t.Errorf("no %s among %v", id, keys(got)) continue } if opening["type"] != "opening" { t.Errorf("%s is a %v", id, opening["type"]) } for k, v := range fields { if opening[k] != v { t.Errorf("%s: %s is %v, want %v", id, k, opening[k], v) } } } for id := range got { if strings.HasPrefix(id, "adoption.opening-") && want[id] == nil { t.Errorf("an opening nothing asked for: %s", id) } } // A port for this machine only opens nothing, and the management port is not opened at all. for id := range got { if strings.Contains(id, "-9000-") || strings.Contains(id, "-15672-") { t.Errorf("%s is opened", id) } } // And openings come first, in the order the machine applies them. if !strings.HasPrefix(composed.Resources[0]["id"].(string), "adoption.opening-") { t.Errorf("openings are not first: %v", composed.Resources[0]["id"]) } } func TestAnAdoptedNodeLoadsNoFilterOfTheMeshs(t *testing.T) { composed, err := anAdoptedAnchor().Compose(anchorRendering(true)) if err != nil { t.Fatal(err) } for _, r := range composed.Resources { if r["path"] == "/etc/nftables.conf" || strings.HasPrefix(r["id"].(string), "nftables.") { t.Fatalf("an adopted node is declared the filter module's %v", r["id"]) } if content, _ := r["content"].(string); strings.Contains(content, "policy drop") { t.Fatalf("an adopted node is declared a table that drops by default: %v", r["id"]) } // Nothing but refusals: the only accept in the guard is its policy. if content, _ := r["content"].(string); r["id"] == GuardID() && strings.Count(content, "accept") != 1 { t.Fatalf("the guard holds an accept:\n%s", content) } } got := byID(composed.Resources) guard := got[GuardID()] if guard == nil || got[GuardUnitID()] == nil || got[GuardRunningID()] == nil { t.Fatalf("no guard: %v", keys(got)) } if guard["content"] != AsGuard([]int{5432, 5672, 15672}) { t.Fatalf("the guard does not guard the store, the broker and its management port:\n%s", guard["content"]) } // The tool that loads it comes first, and the table after it: a node joining adopted has no // filter module and may have no nft. pkg, table := -1, -1 for i, r := range composed.Resources { switch r["id"] { case GuardPackageID(): pkg = i if r["type"] != "package" || r["package"] != "nftables" { t.Fatalf("the guard's package is %v", r) } case GuardID(): table = i } } if pkg < 0 || pkg > table { t.Fatalf("nftables is not declared before the guard's table (%d, %d)", pkg, table) } // A changed table is reloaded — one `nft -f`, atomic — never restarted, which would delete the // table and leave the ports unguarded until it is loaded again. Only a changed unit restarts. if !reflect.DeepEqual(got[GuardRunningID()]["reload-on"], []any{GuardID()}) || !reflect.DeepEqual(got[GuardRunningID()]["restart-on"], []any{GuardUnitID()}) { t.Fatalf("the guard is not reloaded on its table and restarted on its unit: %v", got[GuardRunningID()]) } // Nothing of the mesh's own is anybody's to hold. for id, module := range composed.Owner { if strings.HasPrefix(id, AdoptionPrefix) { t.Fatalf("%s is owned by %s", id, module) } } } func TestAConvergedNodeIsDeclaredItsFilterAndNoOpenings(t *testing.T) { composed, err := anAdoptedAnchor().Compose(anchorRendering(false)) if err != nil { t.Fatal(err) } got := byID(composed.Resources) if got["nftables.filtering"] == nil || got["nftables.load"] == nil { t.Fatalf("a converged node lost its filter: %v", keys(got)) } for id := range got { if strings.HasPrefix(id, AdoptionPrefix) { t.Fatalf("a converged node is declared %s", id) } } plain, err := anAdoptedAnchor().Declaration(anchorRendering(false)) if err != nil { t.Fatal(err) } a, _ := json.Marshal(plain) b, _ := json.Marshal(composed.Resources) if string(a) != string(b) { t.Fatal("Compose and Declaration disagree on a converged node") } } // The table the installer raises and the controller declares, character for character. func TestTheGuardIsExactlyThisTable(t *testing.T) { const golden = `table inet mesh_guard {} delete table inet mesh_guard table inet mesh_guard { chain prerouting { type filter hook prerouting priority raw; policy accept; fib daddr type local iifname != "lo" iifname != "docker0" iifname != "br-*" iifname != "mesh0" tcp dport { 5432, 15672 } drop } } ` if got := AsGuard([]int{15672, 5432}); got != golden { t.Fatalf("the guard changed:\n%s", got) } const unit = `[Unit] Description=The mesh's guard: refuses its own ports from outside (novox/hq ADR 0100) DefaultDependencies=no Wants=network-pre.target Before=network-pre.target shutdown.target Conflicts=shutdown.target [Service] Type=oneshot RemainAfterExit=yes ExecStart=nft -f /etc/mesh/guard.nft ExecReload=nft -f /etc/mesh/guard.nft ExecStop=nft delete table inet mesh_guard [Install] WantedBy=multi-user.target ` if got := GuardUnitText(); got != unit { t.Fatalf("the guard's unit changed:\n%s", got) } if GuardResources(nil) != nil { t.Fatal("a guard with nothing to guard is an empty set nft refuses to load") } } func TestAGuardedPortMustBeAPort(t *testing.T) { if _, err := ParseManifest([]byte(`{"module":"postgres","guards":[5432]}`)); err != nil { t.Fatalf("guards is refused: %v", err) } if _, err := ParseManifest([]byte(`{"module":"postgres","guards":[0]}`)); err == nil { t.Fatal("guarding port 0 was accepted") } } func keys[V any](m map[string]V) []string { return sortedKeys(m) } // novox/hq ADR 0100: the foundation's ports are the node's. Given 5433 for the store, every place // that uses the port reads it from there: the container, the filter, the openings, the guard. func TestAGivenPortIsUsedEverywhereThePortIs(t *testing.T) { given := map[string]map[int]int{"postgres": {5432: 5433}, "lavinmq": {15672: 15673}} for _, adopted := range []bool{true, false} { with := anchorRendering(adopted) with.Given = given with.Ports["postgres"] = map[int]int{5432: 5433} composed, err := anAdoptedAnchor().Compose(with) if err != nil { t.Fatal(err) } got := byID(composed.Resources) if ports := got["postgres.server"]["ports"]; !reflect.DeepEqual(ports, []any{"5433:5432"}) { t.Fatalf("the store's container publishes %v", ports) } if ports := got["lavinmq.server"]["ports"]; !reflect.DeepEqual(ports, []any{"5671:5671", "5672:5672", "127.0.0.1:15673:15672"}) { t.Fatalf("the broker's container publishes %v", ports) } if !adopted { filter, _ := got["nftables.filtering"]["content"].(string) if !strings.Contains(filter, "tcp dport 5433 accept") || strings.Contains(filter, "5432") { t.Fatalf("the filter does not use the given port:\n%s", filter) } continue } if o := got["adoption.opening-tcp-5433-forwarded"]; o == nil || o["to"] != 5432 { t.Fatalf("no opening for the given port: %v", keys(got)) } if guard := got[GuardID()]["content"]; guard != AsGuard([]int{5433, 5672, 15673}) { t.Fatalf("the guard does not guard the given ports:\n%s", guard) } } } func TestAGivenPortIsTheNodesAndReachesSomething(t *testing.T) { store := anAdoptedAnchor().Modules[1] node := func(v any) []Layer { return []Layer{{From: "anchor", Values: map[string]any{PortsSetting: v}}} } if got, err := GivenPorts(store, node(map[string]any{"5432": float64(5433)})); err != nil || got[5432] != 5433 { t.Fatalf("a node's given port was not read: %v %v", got, err) } if _, err := GivenPorts(store, []Layer{{From: MeshWideLayer, Values: map[string]any{PortsSetting: map[string]any{"5432": float64(5433)}}}}); err == nil { t.Fatal("a port given for the whole mesh was accepted") } if _, err := GivenPorts(store, node(map[string]any{"6000": float64(6001)})); err == nil { t.Fatal("a port the module neither listens on, publishes nor guards was given") } if _, err := GivenPorts(store, node(map[string]any{"5432": float64(70000)})); err == nil { t.Fatal("a machine port that is not a port was given") } if _, err := GivenPorts(store, node(map[string]any{"5432": float64(22)})); err == nil { t.Fatal("ssh's port was given") } broker := anAdoptedAnchor().Modules[2] if _, err := GivenPorts(broker, node(map[string]any{"5671": float64(5700), "5672": float64(5700)})); err == nil { t.Fatal("one machine port was given for two of the module's ports") } if stray := UnusedSettings(store, node(map[string]any{"5432": float64(5433)})); len(stray) != 0 { t.Fatalf("a given port is called stray: %v", stray) } } // novox/hq ADR 0103: the guard is derived, and from taken modules only — every machine port a taken // module publishes that the filter admits from the private network only, and the ports its // manifest guards. A module assigned but not taken is not guarded: its port may still be the // predecessor's. func TestTheGuardIsDerivedFromTakenModulesOnly(t *testing.T) { guardOf := func(with Rendering) string { t.Helper() composed, err := anAdoptedAnchor().Compose(with) if err != nil { t.Fatal(err) } content, _ := byID(composed.Resources)[GuardID()]["content"].(string) return content } // The broker taken, the store not: the broker's plain port follows from its listens (from // the mesh, published), its management port from its manifest; the store is not guarded, and // neither is the bus, which the mesh needs from everywhere. with := anchorRendering(true) with.Taken = map[string]bool{"lavinmq": true} if got := guardOf(with); got != AsGuard([]int{5672, 15672}) { t.Fatalf("the guard is not the taken broker's ports:\n%s", got) } // A taken module publishing a port admitted from everywhere is not guarded; one admitted from // the mesh is. The registry is exposed everywhere on this node, and hello-web listens from // everywhere. with.Taken = map[string]bool{"distribution": true, "hello-web": true} if got := guardOf(with); got != "" { t.Fatalf("a port admitted from everywhere is guarded:\n%s", got) } with.Settings = nil if got := guardOf(with); got != AsGuard([]int{5000}) { t.Fatalf("the registry, from the mesh only, is not guarded:\n%s", got) } // Nothing taken, nothing guarded — and no guard at all rather than an empty set. with = anchorRendering(true) with.Taken = nil if got := guardOf(with); got != "" { t.Fatalf("an untaken store is guarded:\n%s", got) } // A given port is followed: where the machine put it is what is refused. with = anchorRendering(true) with.Given = map[string]map[int]int{"lavinmq": {5672: 5682, 15672: 15673}} with.Ports["lavinmq"] = map[int]int{5671: 5671, 5672: 5682} if got := guardOf(with); got != AsGuard([]int{5432, 5682, 15673}) { t.Fatalf("the guard does not follow the given ports:\n%s", got) } } // A mapping bound to loopback is not published to anything off the machine — in either address // family — and an address's own colons never shift the ports. func TestPublishedLeavesOutLoopbackInBothFamilies(t *testing.T) { got := Published([]map[string]any{{"type": "container", "ports": []any{ "127.0.0.1:15672:15672", "[::1]:8080:80", "localhost:9090:90", "[::]:8443:443", "0.0.0.0:5000:5000", "5353:53/udp"}}}) want := map[string]map[int]int{"tcp": {8443: 443, 5000: 5000}, "udp": {5353: 53}} if !reflect.DeepEqual(got, want) { t.Fatalf("published is %v, want %v", got, want) } } // novox/hq ADR 0038: only a published port is the mesh's to move. A module that binds the machine // itself listens where its software was told to, so giving it a machine port is refused. func TestAGivenPortIsRefusedForAPortNoContainerPublishes(t *testing.T) { onTheMachine := Manifest{Module: "daemon", Listens: []Listening{{Port: 9000, From: FromMesh}}, Guards: []int{9000}} layers := []Layer{{From: "node anchor", Values: map[string]any{PortsSetting: map[string]any{"9000": float64(9100)}}}} _, err := GivenPorts(onTheMachine, layers) if err == nil || !strings.Contains(err.Error(), "does not publish") { t.Fatalf("a port no container publishes was given: %v", err) } } // novox/hq ADR 0103: a guarded port this node is told to open to everyone is opened, not guarded. // An opening from everywhere beside a guard dropping it is one statement refusing the other. func TestAGuardedPortOpenedToEveryoneIsNotGuarded(t *testing.T) { with := anchorRendering(true) with.Settings["postgres"] = []Layer{{From: "node anchor", Values: map[string]any{ExposeSetting: map[string]any{"5432": FromEverywhere}}}} composed, err := anAdoptedAnchor().Compose(with) if err != nil { t.Fatal(err) } got := byID(composed.Resources) if o := got["adoption.opening-tcp-5432-forwarded"]; o == nil || o["from"] != OpeningFromEverywhere { t.Fatalf("the store's port is not opened to everyone: %v", o) } if guard, _ := got[GuardID()]["content"].(string); guard != AsGuard([]int{5672, 15672}) { t.Fatalf("a port opened to everyone is still guarded:\n%s", guard) } } // A module that publishes its ssh port the long way — `2222:22`, because the machine's own daemon // holds 22 — and says it listens on the machine side of that mapping, which is what anything // reaching it dials. func aForge() Manifest { return Manifest{Module: "forge", Provides: []Offer{{Name: "git-over-ssh", Scope: ScopeMesh}}, Serves: map[string]map[string]any{"git-over-ssh": {"port": 2222}}, Listens: []Listening{{Port: 3000, From: FromMesh}, {Port: 2222, From: FromMesh}}, Resources: []map[string]any{{"id": "server", "type": "container", "name": "forge", "ports": []any{"3000", "2222:22"}}}} } // portsAsThePlanWould is where this machine puts each of a module's ports, derived the way // cmd/mesh-controller/plan.go derives it: a given port first, looked up by the port the module // says it listens on, and otherwise wherever the manifest's own mapping already put it. Written // here because everything below — the filter, the openings, the guard, what a consumer is told — // reads that map, and a given port that the lookup does not find moves the container's mapping // and nothing else. // // It stands in for the plan only where the plan does not allocate: a port the manifest already // placed, or one a node was given. For a short form with no given port the real plan asks the // inventory for a machine port and may come back with one from the pool, which needs a store and // is what cmd/mesh-controller's own tests exercise. So an assertion here about such a port asserts // this helper, not the mesh; keep the assertions to the ports under test. func portsAsThePlanWould(m Manifest, given map[int]int) map[int]int { out := map[int]int{} for _, l := range m.Listens { if at, is := given[l.Port]; is { out[l.Port] = at continue } at, _ := m.MachineSide(l.Port) out[l.Port] = at } return out } // novox/hq ADR 0100 and 0038: the machine side of a long-form mapping is a port the module // publishes, so a node may move it — and a module may name a mapping by either end. func TestAGivenPortNamesEitherEndOfWhatTheModulePublishes(t *testing.T) { forge := aForge() node := func(v any) []Layer { return []Layer{{From: "node anchor", Values: map[string]any{PortsSetting: v}}} } // The machine side — the number this module says it listens on, and the one the predecessor // had somewhere else. Answered under 2222, the end the module itself names, which is what // every reader of this map asks for. One entry, not two: a second key for the same answer is // an entry another mapping's reader could find instead. given, err := GivenPorts(forge, node(map[string]any{"2222": float64(222)})) if err != nil { t.Fatalf("the machine side of a mapping cannot be given a port: %v", err) } if want := map[int]int{2222: 222}; !reflect.DeepEqual(given, want) { t.Fatalf("the forge was given %v; the mapping it names is filed under %v", given, want) } // The container's own port names the same mapping and means the same thing — and is filed // under the same name, because the module's name for it has not changed. if inside, err := GivenPorts(forge, node(map[string]any{"22": float64(222)})); err != nil || !reflect.DeepEqual(inside, map[int]int{2222: 222}) { t.Fatalf("the container's end of the mapping was given %v: %v", inside, err) } // A short form is published on the number it names, and is unchanged by any of this. if short, err := GivenPorts(forge, node(map[string]any{"3000": float64(2999)})); err != nil || short[3000] != 2999 { t.Fatalf("the short form was given %v: %v", short, err) } // A port no container publishes is still refused, in the same words. if _, err := GivenPorts(forge, node(map[string]any{"9000": float64(9100)})); err == nil || !strings.Contains(err.Error(), "does not publish") { t.Fatalf("a port the forge does not publish was given: %v", err) } // And the two ends of one mapping given two different numbers is one setting contradicting // the other: the machine publishes it once. if _, err := GivenPorts(forge, node(map[string]any{ "2222": float64(222), "22": float64(300)})); err == nil || !strings.Contains(err.Error(), "one mapping") { t.Fatalf("the two ends of one mapping were given different ports: %v", err) } // Said at both ends with the same number, it is still said twice, and refused where every // other repeated machine port is — as the inventory refuses it when the setting is stored, // which is the layer that sees it first. if _, err := GivenPorts(forge, node(map[string]any{ "2222": float64(222), "22": float64(222)})); err == nil || !strings.Contains(err.Error(), "to both its 22 and its 2222") { t.Fatalf("one mapping given one machine port at both ends: %v", err) } // A number that names two different mappings names neither: which one to move is not said. twice := aForge() twice.Resources[0]["ports"] = []any{"22", "2222:22"} if _, err := GivenPorts(twice, node(map[string]any{"22": float64(222)})); err == nil || !strings.Contains(err.Error(), "twice") { t.Fatalf("a number naming two of the module's mappings was accepted: %v", err) } // And the same refusal when the number naming two mappings is not the one the setting used // but the one the answer would be filed under. Here `80` is the module's own name for a // mapping, and two mappings wear it; filing an answer there is one container's port standing // where the other's is read, and both containers then publish it. shared := Manifest{Module: "gallery", Listens: []Listening{{Port: 80, From: FromMesh}}, Resources: []map[string]any{ {"id": "a", "type": "container", "name": "a", "ports": []any{"4001:80"}}, {"id": "b", "type": "container", "name": "b", "ports": []any{"4002:80"}}}} if _, err := GivenPorts(shared, node(map[string]any{"4001": float64(1234)})); err == nil || !strings.Contains(err.Error(), "twice") { t.Fatalf("two containers were put on one machine port: %v", err) } // A module whose mappings chain — one's machine side is another's container port — keeps them // apart, because each is filed under the port the module names it by and neither name is // shared. Given both, each moves on its own and neither overwrites the other. chained := Manifest{Module: "chain", Listens: []Listening{{Port: 80, From: FromMesh}, {Port: 9090, From: FromMesh}}, Resources: []map[string]any{{"id": "server", "type": "container", "name": "chain", "ports": []any{"8080:80", "9090:8080"}}}} both, err := GivenPorts(chained, node(map[string]any{"80": float64(1234), "9090": float64(5678)})) if err != nil || !reflect.DeepEqual(both, map[int]int{80: 1234, 9090: 5678}) { t.Fatalf("chained mappings were given %v: %v", both, err) } if moved := givenOuter("8080:80", both); moved != "1234:80" { t.Fatalf("the first mapping moved to %q, and it was given 1234", moved) } if moved := givenOuter("9090:8080", both); moved != "5678:8080" { t.Fatalf("the second mapping moved to %q, and it was given 5678", moved) } } // And the number reaches everything derived from it. The fault this is written against moved the // container's mapping alone: the filter opened the port the software had left, the adopted node's // opening named it too, the guard refused it, and a consumer was sent to it. func TestAGivenMachineSideReachesTheFilterTheOpeningAndTheConsumer(t *testing.T) { forge := aForge() given, err := GivenPorts(forge, []Layer{{From: "node anchor", Values: map[string]any{PortsSetting: map[string]any{"2222": float64(222)}}}}) if err != nil { t.Fatalf("the machine side of a mapping cannot be given a port: %v", err) } r := Resolution{Node: "anchor", Modules: []Manifest{forge}} with := Rendering{ Ports: map[string]map[int]int{"forge": portsAsThePlanWould(forge, given)}, Given: map[string]map[int]int{"forge": given}, Mesh: []string{"10.77.0.1"}, Adopted: true, Taken: map[string]bool{"forge": true}, } // What the runtime is handed: the machine's own port on the outside, the container's within. composed, err := r.Compose(with) if err != nil { t.Fatalf("the forge does not compose: %v", err) } got := byID(composed.Resources) if ports := got["forge.server"]["ports"]; !reflect.DeepEqual(ports, []any{"3000:3000", "222:22"}) { t.Fatalf("the forge's container publishes %v", ports) } // What the filter would open, were the node converged. rules, err := r.Rules(with) if err != nil { t.Fatal(err) } var opened []int for _, rule := range rules { opened = append(opened, rule.Port) } // Only the moved port is asserted: the forge's other port is a short form the real plan would // allocate rather than read off the manifest, so its number here is portsAsThePlanWould's and // not the mesh's. if !slices.Contains(opened, 222) || slices.Contains(opened, 2222) { t.Fatalf("the filter opens %v, not where the machine puts the forge", opened) } // And what it is declared instead, adopted: an opening on the machine's port, naming the // container's port on the forwarded path — a published port is forwarded, never received. opening := got[OpeningID("tcp", 222, PathForwarded)] if opening == nil || opening["to"] != 22 || opening["from"] != OpeningFromMesh { t.Fatalf("no opening for the port this node gave the forge: %v", keys(got)) } for id := range got { if strings.HasPrefix(id, "adoption.opening-tcp-2222-") { t.Errorf("an opening for the port the forge was moved off: %s", id) } } // The guard refuses it where the machine put it, and nothing where it used to be. guard, _ := got[GuardID()]["content"].(string) if !strings.Contains(guard, "222") || strings.Contains(guard, "2222") { t.Fatalf("the guard does not follow the given port:\n%s", guard) } // And a consumer is sent to the same number, which is read from what the module serves. if told := ServedOn(forge, "git-over-ssh", with.Ports["forge"])["port"]; told != 222 { t.Fatalf("a consumer is told the forge answers on %v", told) } } // And the mapping itself moves under either name, because Rendering.Given is a map anybody // composing a declaration hands in: keyed by the machine side, which is what a module declaring // 2222 calls its port, only the outside moves and the container's own port stays as written. func TestAMappingIsMovedUnderEitherOfItsNames(t *testing.T) { for _, c := range []struct { written string given map[int]int want string }{ {"2222:22", map[int]int{2222: 222}, "222:22"}, {"2222:22", map[int]int{22: 222}, "222:22"}, {"127.0.0.1:15672:15672/tcp", map[int]int{15672: 15673}, "127.0.0.1:15673:15672/tcp"}, {"2222:22", map[int]int{3000: 2999}, "2222:22"}, {"2222:22", nil, "2222:22"}, } { if got := givenOuter(c.written, c.given); got != c.want { t.Errorf("%s given %v is published as %s, want %s", c.written, c.given, got, c.want) } } } // The same on a node that was given nothing, which is where the derivation was never at fault: // a long-form mapping is published where the manifest put it, and an adopted node opens that port // on the forwarded path like any other. What broke it was the number reaching only the mapping. func TestALongFormPortIsOpenedWhereTheManifestPublishesIt(t *testing.T) { forge := aForge() r := Resolution{Node: "anchor", Modules: []Manifest{forge}} composed, err := r.Compose(Rendering{ Ports: map[string]map[int]int{"forge": portsAsThePlanWould(forge, nil)}, Mesh: []string{"10.77.0.1"}, Adopted: true, }) if err != nil { t.Fatal(err) } got := byID(composed.Resources) opening := got[OpeningID("tcp", 2222, PathForwarded)] if opening == nil || opening["to"] != 22 { t.Fatalf("no opening for the forge's published ssh port: %v", keys(got)) } }