One entry per mapping, under the name the module itself uses
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.
This commit is contained in:
@@ -330,11 +330,15 @@ func TestTheMachineSideOfAMappingIsMovedEverywhereTheNumberIsUsed(t *testing.T)
|
|||||||
if opening == nil || opening["to"] != 22 || opening["from"] != catalogue.OpeningFromMesh {
|
if opening == nil || opening["to"] != 22 || opening["from"] != catalogue.OpeningFromMesh {
|
||||||
t.Fatalf("no opening for the port this node gave the forge: %v", opening)
|
t.Fatalf("no opening for the port this node gave the forge: %v", opening)
|
||||||
}
|
}
|
||||||
|
var guard map[string]any
|
||||||
for _, r := range resources {
|
for _, r := range resources {
|
||||||
if r["id"] == catalogue.GuardID() && r["content"] != catalogue.AsGuard([]int{222}) {
|
if r["id"] == catalogue.GuardID() {
|
||||||
t.Fatalf("the guard does not refuse the port the forge is on:\n%s", r["content"])
|
guard = r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if guard == nil || guard["content"] != catalogue.AsGuard([]int{222}) {
|
||||||
|
t.Fatalf("the guard does not refuse the port the forge is on:\n%v", guard["content"])
|
||||||
|
}
|
||||||
|
|
||||||
// And the consumer on the other machine dials the same number.
|
// And the consumer on the other machine dials the same number.
|
||||||
plan, _, err := planFor(ctx, open, "laptop")
|
plan, _, err := planFor(ctx, open, "laptop")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package catalogue
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -448,6 +449,12 @@ func aForge() Manifest {
|
|||||||
// here because everything below — the filter, the openings, the guard, what a consumer is told —
|
// 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
|
// reads that map, and a given port that the lookup does not find moves the container's mapping
|
||||||
// and nothing else.
|
// 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 {
|
func portsAsThePlanWould(m Manifest, given map[int]int) map[int]int {
|
||||||
out := map[int]int{}
|
out := map[int]int{}
|
||||||
for _, l := range m.Listens {
|
for _, l := range m.Listens {
|
||||||
@@ -470,19 +477,21 @@ func TestAGivenPortNamesEitherEndOfWhatTheModulePublishes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The machine side — the number this module says it listens on, and the one the predecessor
|
// The machine side — the number this module says it listens on, and the one the predecessor
|
||||||
// had somewhere else. Answered under both ends, because the plan looks a given port up by the
|
// had somewhere else. Answered under 2222, the end the module itself names, which is what
|
||||||
// port the module declares and the container's mapping is rewritten by the port inside it.
|
// 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)}))
|
given, err := GivenPorts(forge, node(map[string]any{"2222": float64(222)}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("the machine side of a mapping cannot be given a port: %v", err)
|
t.Fatalf("the machine side of a mapping cannot be given a port: %v", err)
|
||||||
}
|
}
|
||||||
if given[2222] != 222 || given[22] != 222 {
|
if want := map[int]int{2222: 222}; !reflect.DeepEqual(given, want) {
|
||||||
t.Fatalf("the forge was given %v, and its mapping has two ends", given)
|
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.
|
// 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 ||
|
if inside, err := GivenPorts(forge, node(map[string]any{"22": float64(222)})); err != nil ||
|
||||||
inside[2222] != 222 || inside[22] != 222 {
|
!reflect.DeepEqual(inside, map[int]int{2222: 222}) {
|
||||||
t.Fatalf("the container's end of the mapping was given %v: %v", inside, err)
|
t.Fatalf("the container's end of the mapping was given %v: %v", inside, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,6 +529,38 @@ func TestAGivenPortNamesEitherEndOfWhatTheModulePublishes(t *testing.T) {
|
|||||||
!strings.Contains(err.Error(), "twice") {
|
!strings.Contains(err.Error(), "twice") {
|
||||||
t.Fatalf("a number naming two of the module's mappings was accepted: %v", err)
|
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
|
// And the number reaches everything derived from it. The fault this is written against moved the
|
||||||
@@ -560,7 +601,10 @@ func TestAGivenMachineSideReachesTheFilterTheOpeningAndTheConsumer(t *testing.T)
|
|||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
opened = append(opened, rule.Port)
|
opened = append(opened, rule.Port)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(opened, []int{222, 3000}) {
|
// 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)
|
t.Fatalf("the filter opens %v, not where the machine puts the forge", opened)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,7 +621,8 @@ func TestAGivenMachineSideReachesTheFilterTheOpeningAndTheConsumer(t *testing.T)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The guard refuses it where the machine put it, and nothing where it used to be.
|
// The guard refuses it where the machine put it, and nothing where it used to be.
|
||||||
if guard, _ := got[GuardID()]["content"].(string); guard != AsGuard([]int{222, 3000}) {
|
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)
|
t.Fatalf("the guard does not follow the given port:\n%s", guard)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package catalogue
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -484,14 +485,19 @@ const MeshWideLayer = "the mesh"
|
|||||||
// what consumers are told while the software still listens on the old one — a port that reads as
|
// what consumers are told while the software still listens on the old one — a port that reads as
|
||||||
// moved and is not.
|
// moved and is not.
|
||||||
//
|
//
|
||||||
// **Either name of a mapping names it.** A short form publishes one number, which is the
|
// **Either name of a mapping names it; the module's own name answers.** A short form publishes one
|
||||||
// container's port and the machine's at once. A mapping written the long way — `"2222:22"` — has
|
// number, which is the container's port and the machine's at once. A mapping written the long way
|
||||||
// two, and a module reasonably declares it listens on either: the port its software uses, or the
|
// — `"2222:22"` — has two, and a module reasonably declares it listens on either: the port its
|
||||||
// port the machine already serves on. Both are accepted, and both come back, so that whoever
|
// software uses, or the port the machine already serves on. A setting may name either end, because
|
||||||
// reads this — the ports map, the filter, the openings, what a consumer is told, and the mapping
|
// both are true of the same mapping. The answer comes back under the end the **module** names in
|
||||||
// the runtime is handed — finds the same number under the key it happens to hold. Keyed one way
|
// its `listens`, which is the number every reader of this map holds: the ports map, the filter, the
|
||||||
// and read the other, the setting moved the container's mapping and nothing else: a firewall,
|
// openings, what a consumer is told, and the mapping the runtime is handed. Keyed one way and read
|
||||||
// a set of openings and a consumer all pointing at a port the software had left.
|
// the other, the setting moved the container's mapping and nothing else — a firewall, a set of
|
||||||
|
// openings and a consumer all pointing at a port the software had left.
|
||||||
|
//
|
||||||
|
// One entry per mapping, never two. A second key for the same answer is not a convenience: where
|
||||||
|
// two mappings share a number, it is an entry one of them writes over the other's, and the reader
|
||||||
|
// that finds the survivor disagrees with the reader that recomputes it.
|
||||||
func GivenPorts(m Manifest, layers []Layer) (map[int]int, error) {
|
func GivenPorts(m Manifest, layers []Layer) (map[int]int, error) {
|
||||||
// Every name a setting may use, and the mapping it names.
|
// Every name a setting may use, and the mapping it names.
|
||||||
names := map[int][]publishing{}
|
names := map[int][]publishing{}
|
||||||
@@ -501,8 +507,16 @@ func GivenPorts(m Manifest, layers []Layer) (map[int]int, error) {
|
|||||||
names[p.machine] = append(names[p.machine], p)
|
names[p.machine] = append(names[p.machine], p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// And the names the module itself uses. A mapping's answer is filed under these, because they
|
||||||
|
// are what every reader asks for; a mapping the module names at neither end is filed under its
|
||||||
|
// machine side, where nothing looks, which is correct — nothing serves it.
|
||||||
|
declares := map[int]bool{}
|
||||||
|
for _, l := range m.Listens {
|
||||||
|
declares[l.Port] = true
|
||||||
|
}
|
||||||
chose := map[int]int{} // the port a setting named → the machine port it gave it
|
chose := map[int]int{} // the port a setting named → the machine port it gave it
|
||||||
meant := map[int]publishing{} // and which of the module's mappings that was
|
out := map[int]int{} // the port the module names → the machine port it is on
|
||||||
|
by := map[int]int{} // and which of the setting's ports put it there, for the refusal
|
||||||
for _, layer := range layers {
|
for _, layer := range layers {
|
||||||
raw, ok := layer.Values[PortsSetting]
|
raw, ok := layer.Values[PortsSetting]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -528,15 +542,8 @@ func GivenPorts(m Manifest, layers []Layer) (map[int]int, error) {
|
|||||||
"publishes %d — the mesh cannot move a port the module does not publish; the "+
|
"publishes %d — the mesh cannot move a port the module does not publish; the "+
|
||||||
"software would go on listening where it was told to", m.Module, port, port)
|
"software would go on listening where it was told to", m.Module, port, port)
|
||||||
}
|
}
|
||||||
// The same number naming two different mappings — `"22"` beside `"2222:22"`, say.
|
if err := oneMapping(m.Module, port, publishes); err != nil {
|
||||||
// Refused rather than picked: moving one of them and leaving the other is a mapping
|
return nil, err
|
||||||
// the operator did not ask for and cannot see, and the two readings differ.
|
|
||||||
for _, other := range publishes[1:] {
|
|
||||||
if other != publishes[0] {
|
|
||||||
return nil, fmt.Errorf("%s gives port %d a machine port, and its containers "+
|
|
||||||
"publish %d twice — as %s and as %s; which one to move is not said",
|
|
||||||
m.Module, port, port, publishes[0], other)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
at, ok := asPort(value)
|
at, ok := asPort(value)
|
||||||
if !ok || at < 1 || at > 65535 {
|
if !ok || at < 1 || at > 65535 {
|
||||||
@@ -548,13 +555,11 @@ func GivenPorts(m Manifest, layers []Layer) (map[int]int, error) {
|
|||||||
"one port a machine may never lose", m.Module, port, at)
|
"one port a machine may never lose", m.Module, port, at)
|
||||||
}
|
}
|
||||||
chose[port] = at
|
chose[port] = at
|
||||||
meant[port] = publishes[0]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// One holder per machine port, within the module too — which also settles a mapping named at
|
// One holder per machine port, within the module too.
|
||||||
// both ends, because the machine publishes it once whichever end the setting called it.
|
|
||||||
holder := map[int]int{}
|
holder := map[int]int{}
|
||||||
for _, port := range sorted(chose) {
|
for _, port := range sortedPorts(chose) {
|
||||||
at := chose[port]
|
at := chose[port]
|
||||||
if other, twice := holder[at]; twice {
|
if other, twice := holder[at]; twice {
|
||||||
return nil, fmt.Errorf("%s gives machine port %d to both its %d and its %d", m.Module,
|
return nil, fmt.Errorf("%s gives machine port %d to both its %d and its %d", m.Module,
|
||||||
@@ -562,21 +567,35 @@ func GivenPorts(m Manifest, layers []Layer) (map[int]int, error) {
|
|||||||
}
|
}
|
||||||
holder[at] = port
|
holder[at] = port
|
||||||
}
|
}
|
||||||
// And one machine port per mapping: `{"22": 222, "2222": 300}` is two numbers for the one
|
// Filed under the module's own names for the mapping — every one it uses, so a module that
|
||||||
// thing the machine publishes, and neither is more right.
|
// says it listens on both ends is answered at both, and under the machine side when it names
|
||||||
out := map[int]int{}
|
// neither.
|
||||||
said := map[publishing]int{}
|
for _, port := range sortedPorts(chose) {
|
||||||
for _, port := range sorted(chose) {
|
at, mapping := chose[port], names[port][0]
|
||||||
at, mapped := chose[port], meant[port]
|
keys := []int{}
|
||||||
if was, twice := said[mapped]; twice {
|
for _, end := range []int{mapping.machine, mapping.inner} {
|
||||||
return nil, fmt.Errorf("%s gives %s the machine ports %d and %d — its %d and its %d "+
|
if declares[end] && !slices.Contains(keys, end) {
|
||||||
"are the two ends of one mapping, and it is published once", m.Module, mapped,
|
keys = append(keys, end)
|
||||||
min(was, at), max(was, at), min(mapped.machine, mapped.inner),
|
}
|
||||||
max(mapped.machine, mapped.inner))
|
}
|
||||||
|
if len(keys) == 0 {
|
||||||
|
keys = []int{mapping.machine}
|
||||||
|
}
|
||||||
|
for _, key := range keys {
|
||||||
|
// The key must name this mapping and no other, or the entry is one mapping's answer
|
||||||
|
// standing where another's is read.
|
||||||
|
if err := oneMapping(m.Module, key, names[key]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// And one machine port per mapping: `{"22": 222, "2222": 300}` is two numbers for the
|
||||||
|
// one thing the machine publishes, and neither is more right.
|
||||||
|
if was, twice := out[key]; twice && was != at {
|
||||||
|
return nil, fmt.Errorf("%s gives %s the machine ports %d and %d — its %d and its "+
|
||||||
|
"%d are the two ends of one mapping, and it is published once", m.Module,
|
||||||
|
mapping, min(was, at), max(was, at), min(by[key], port), max(by[key], port))
|
||||||
|
}
|
||||||
|
out[key], by[key] = at, port
|
||||||
}
|
}
|
||||||
said[mapped] = at
|
|
||||||
out[mapped.inner] = at
|
|
||||||
out[mapped.machine] = at
|
|
||||||
}
|
}
|
||||||
if len(out) == 0 {
|
if len(out) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -584,6 +603,21 @@ func GivenPorts(m Manifest, layers []Layer) (map[int]int, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// oneMapping refuses a number that names two of a module's mappings — `"22"` beside `"2222:22"`,
|
||||||
|
// say, or `"4001:80"` beside `"4002:80"` read by their shared `80`. Refused rather than picked:
|
||||||
|
// moving one of them and leaving the other is a mapping the operator did not ask for and cannot
|
||||||
|
// see, and the two readings differ.
|
||||||
|
func oneMapping(module string, port int, publishes []publishing) error {
|
||||||
|
for _, other := range publishes[1:] {
|
||||||
|
if other != publishes[0] {
|
||||||
|
return fmt.Errorf("%s gives port %d a machine port, and its containers publish %d "+
|
||||||
|
"twice — as %s and as %s; which one to move is not said", module, port, port,
|
||||||
|
publishes[0], other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// publishing is one mapping a module's container writes: the port the container itself uses, and
|
// publishing is one mapping a module's container writes: the port the container itself uses, and
|
||||||
// the machine port the manifest put it on — the same number for a short form, which the runtime
|
// the machine port the manifest put it on — the same number for a short form, which the runtime
|
||||||
// publishes on the port it names.
|
// publishes on the port it names.
|
||||||
@@ -621,8 +655,8 @@ func publishedPorts(m Manifest) []publishing {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// sorted is a settings map's ports in order, so a refusal reads the same on every run.
|
// sortedPorts is a settings map's ports in order, so a refusal reads the same on every run.
|
||||||
func sorted(of map[int]int) []int {
|
func sortedPorts(of map[int]int) []int {
|
||||||
out := make([]int, 0, len(of))
|
out := make([]int, 0, len(of))
|
||||||
for port := range of {
|
for port := range of {
|
||||||
out = append(out, port)
|
out = append(out, port)
|
||||||
|
|||||||
@@ -266,10 +266,10 @@ func TestTheForgesSshPortIsGivenByTheNumberTheForgeCallsIt(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("the forge's ssh port cannot be given on a node: %v", err)
|
t.Fatalf("the forge's ssh port cannot be given on a node: %v", err)
|
||||||
}
|
}
|
||||||
// Under the number the module listens on, which is how the plan finds it, and under the
|
// Under the number the module listens on — 2222, the machine side of its mapping — which is
|
||||||
// container's own port, which is how the mapping is rewritten.
|
// the number the plan, the filter, the openings and the consumer all ask for. One entry.
|
||||||
if given[2222] != 222 || given[22] != 222 {
|
if want := map[int]int{2222: 222}; !reflect.DeepEqual(given, want) {
|
||||||
t.Fatalf("the forge was given %v", given)
|
t.Fatalf("the forge was given %v, and it names its ssh port %v", given, want)
|
||||||
}
|
}
|
||||||
|
|
||||||
resolved, err := forge.Resolve([]Built{{
|
resolved, err := forge.Resolve([]Built{{
|
||||||
|
|||||||
Reference in New Issue
Block a user