Review found the first pass aliased its answer under both ends of a mapping, which is wrong wherever two mappings share a number: the alias lands on a key belonging to another mapping, the later write wins, and the filter and the container then disagree — the very fault this change exists to close. Two reproduced cases: a module publishing 8080:80 beside 9090:8080 had an explicit setting silently overwritten; a module publishing 4001:80 beside 4002:80 composed both containers onto one machine port, where before it was safely refused. Now a mapping's answer is filed once, under the end the module names in its listens — the number the plan, the filter, the openings, the guard and the consumer all ask for — and a key that names two mappings is refused in the same words as a setting that does. Also: the guard assertion in the end-to-end test failed open when the resource was absent; the plan-mirroring helper now says it stands in only where the plan does not allocate, and the assertions it feeds are narrowed to the port under test.
676 lines
28 KiB
Go
676 lines
28 KiB
Go
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))
|
|
}
|
|
}
|