Read the foundation's ports from each node's settings, wherever a port is used (hq ADR 0100)
This commit is contained in:
@@ -221,3 +221,63 @@ func TestAGuardedPortMustBeAPort(t *testing.T) {
|
||||
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, 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 stray := UnusedSettings(store, node(map[string]any{"5432": float64(5433)})); len(stray) != 0 {
|
||||
t.Fatalf("a given port is called stray: %v", stray)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,12 +133,20 @@ type Rendering struct {
|
||||
// force, so no module that loads a filter is declared there, and what the mesh needs
|
||||
// reachable is declared as openings, with its own ports guarded by a table that only refuses.
|
||||
Adopted bool
|
||||
|
||||
// Given is the machine ports this node was given for its modules' ports, by module and by the
|
||||
// port the software uses (novox/hq ADR 0100) — the foundation's ports, as genesis chose them.
|
||||
// They win over anything the mesh would assign and over a manifest's own long-form mapping.
|
||||
Given map[string]map[int]int
|
||||
}
|
||||
|
||||
// machinePort is where a module's port lives on this machine, or the port itself when the mesh has
|
||||
// not been asked. Unassigned is not an error here: a module with no `listens` never needed one,
|
||||
// and a caller composing a declaration without a store still gets something coherent.
|
||||
func (r Rendering) machinePort(module string, wanted int) int {
|
||||
if at, given := r.Given[module][wanted]; given {
|
||||
return at
|
||||
}
|
||||
if at, known := r.Ports[module][wanted]; known {
|
||||
return at
|
||||
}
|
||||
@@ -1188,7 +1196,11 @@ func publishedOn(resource map[string]any, module string, with Rendering) {
|
||||
for _, entry := range listed {
|
||||
written := fmt.Sprint(entry)
|
||||
if strings.Contains(written, ":") {
|
||||
out = append(out, written)
|
||||
// Written the long way, and left alone — unless this node was given a machine port for
|
||||
// it (novox/hq ADR 0100): the foundation's ports are the node's, and a manifest's
|
||||
// number is only the default. The outer port only; an address and the software's
|
||||
// port stay as written.
|
||||
out = append(out, givenOuter(written, with.Given[module]))
|
||||
continue
|
||||
}
|
||||
wanted, err := strconv.Atoi(strings.TrimSpace(written))
|
||||
@@ -1203,6 +1215,29 @@ func publishedOn(resource map[string]any, module string, with Rendering) {
|
||||
resource["ports"] = out
|
||||
}
|
||||
|
||||
// givenOuter rewrites the machine side of a long-form mapping to the port this node was given for
|
||||
// its software side, when it was given one.
|
||||
func givenOuter(written string, given map[int]int) string {
|
||||
if len(given) == 0 {
|
||||
return written
|
||||
}
|
||||
mapping, protocol := written, ""
|
||||
if cut := strings.LastIndex(written, "/"); cut >= 0 {
|
||||
mapping, protocol = written[:cut], written[cut:]
|
||||
}
|
||||
parts := strings.Split(mapping, ":")
|
||||
inner, err := strconv.Atoi(strings.TrimSpace(parts[len(parts)-1]))
|
||||
if err != nil {
|
||||
return written
|
||||
}
|
||||
at, ok := given[inner]
|
||||
if !ok {
|
||||
return written
|
||||
}
|
||||
parts[len(parts)-2] = strconv.Itoa(at)
|
||||
return strings.Join(parts, ":") + protocol
|
||||
}
|
||||
|
||||
// ServedOn is what a provider tells a consumer, with the port that machine actually uses.
|
||||
//
|
||||
// **The module writes the port once, in `listens`** (novox/hq ADR 0038). It used to write it three
|
||||
|
||||
@@ -457,3 +457,95 @@ func byFamily(addresses []string) (four []string, six []string) {
|
||||
}
|
||||
return four, six
|
||||
}
|
||||
|
||||
// PortsSetting is the settings key that gives a module's port a machine port on one node (novox/hq
|
||||
// ADR 0100):
|
||||
//
|
||||
// {"ports": {"5432": 5433}}
|
||||
//
|
||||
// puts what the software calls 5432 on the machine's 5433. The foundation's ports are the node's:
|
||||
// every one is an input to genesis, checked free there, and becomes that node's setting for the
|
||||
// foundation's modules — the catalogue's numbers are only their defaults. Keyed by the port the
|
||||
// software uses, like expose; the value is where the machine puts it.
|
||||
const PortsSetting = "ports"
|
||||
|
||||
// MeshWideLayer is what a layer set for the whole mesh is called, rather than for one node.
|
||||
const MeshWideLayer = "the mesh"
|
||||
|
||||
// GivenPorts reads a module's given machine ports from its settings: software port → machine port.
|
||||
//
|
||||
// Refused from a mesh-wide layer — a port is a fact about one machine, and one number for every
|
||||
// machine is the collision this exists to avoid — and for a port the module neither listens on,
|
||||
// publishes from a container, nor guards: a given port that reaches nothing is a setting somebody
|
||||
// believes changed something.
|
||||
func GivenPorts(m Manifest, layers []Layer) (map[int]int, error) {
|
||||
known := map[int]bool{}
|
||||
for _, l := range m.Listens {
|
||||
known[l.Port] = true
|
||||
}
|
||||
for _, p := range m.Guards {
|
||||
known[p] = true
|
||||
}
|
||||
for _, p := range containerPorts(m) {
|
||||
known[p] = true
|
||||
}
|
||||
out := map[int]int{}
|
||||
for _, layer := range layers {
|
||||
raw, ok := layer.Values[PortsSetting]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if layer.From == MeshWideLayer {
|
||||
return nil, fmt.Errorf("%s: %s is given per node — a port is a fact about one "+
|
||||
"machine; set it with --node", m.Module, PortsSetting)
|
||||
}
|
||||
entries, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s: %s is a { port: machine-port } map, and %q set it to "+
|
||||
"something else", m.Module, PortsSetting, layer.From)
|
||||
}
|
||||
for portText, value := range entries {
|
||||
port, err := strconv.Atoi(portText)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s gives %q a port, which is not a port", m.Module, portText)
|
||||
}
|
||||
if !known[port] {
|
||||
return nil, fmt.Errorf("%s gives port %d a machine port, and it neither listens "+
|
||||
"on, publishes nor guards %d — the setting reaches nothing", m.Module, port, port)
|
||||
}
|
||||
at, ok := asPort(value)
|
||||
if !ok || at < 1 || at > 65535 {
|
||||
return nil, fmt.Errorf("%s gives port %d the machine port %v, which is not a port",
|
||||
m.Module, port, value)
|
||||
}
|
||||
out[port] = at
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// containerPorts are the software ports a module's containers publish, whichever form they are
|
||||
// written in.
|
||||
func containerPorts(m Manifest) []int {
|
||||
var out []int
|
||||
for _, r := range m.Resources {
|
||||
if fmt.Sprint(r["type"]) != "container" {
|
||||
continue
|
||||
}
|
||||
listed, _ := r["ports"].([]any)
|
||||
for _, entry := range listed {
|
||||
written := strings.TrimSpace(fmt.Sprint(entry))
|
||||
if cut := strings.LastIndex(written, "/"); cut >= 0 {
|
||||
written = written[:cut]
|
||||
}
|
||||
parts := strings.Split(written, ":")
|
||||
if n, err := strconv.Atoi(parts[len(parts)-1]); err == nil {
|
||||
out = append(out, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -101,6 +101,11 @@ func settle(base map[string]any, layers []Layer, protected map[string]bool, what
|
||||
merged := deepCopy(base)
|
||||
for _, layer := range layers {
|
||||
for key, value := range layer.Values {
|
||||
if key == PortsSetting {
|
||||
// Where the machine puts a port is the mesh's to apply, not a value for a file or
|
||||
// for what a consumer is told (novox/hq ADR 0100); it reaches both as the port.
|
||||
continue
|
||||
}
|
||||
if protected[key] {
|
||||
// The module said it must own this one. Refused rather than ignored: a setting
|
||||
// that is quietly dropped is somebody believing they changed something.
|
||||
@@ -176,6 +181,11 @@ func UnusedSettings(m Manifest, layers []Layer) []string {
|
||||
if key == ExposeSetting && len(m.Listens) > 0 {
|
||||
continue
|
||||
}
|
||||
// `ports` gives a module's port a machine port on one node (novox/hq ADR 0100),
|
||||
// validated in GivenPorts, so it is not stray here either.
|
||||
if key == PortsSetting {
|
||||
continue
|
||||
}
|
||||
unused = append(unused, fmt.Sprintf(
|
||||
"%s sets %q, and %s has no file or contribution to merge it into",
|
||||
layer.From, key, m.Module))
|
||||
|
||||
@@ -654,7 +654,7 @@ func (i *Inventory) SettingsFor(ctx context.Context, nodeName, module string) ([
|
||||
}
|
||||
from := nodeName
|
||||
if meshWide {
|
||||
from = "the mesh"
|
||||
from = catalogue.MeshWideLayer
|
||||
}
|
||||
layers = append(layers, catalogue.Layer{From: from, Values: values})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
@@ -132,6 +133,17 @@ func (i *Inventory) assignPort(
|
||||
taken[port] = "something this machine already runs"
|
||||
}
|
||||
}
|
||||
// And every port this machine was given for a module (novox/hq ADR 0100): the foundation's
|
||||
// ports, as genesis chose them, are the node's settings and never the mesh's to hand out.
|
||||
given, err := i.givenOn(ctx, nodeID)
|
||||
if err != nil {
|
||||
return Assigned{}, err
|
||||
}
|
||||
for port, by := range given {
|
||||
if _, mine := taken[port]; !mine {
|
||||
taken[port] = by
|
||||
}
|
||||
}
|
||||
|
||||
machine := wanted
|
||||
if !fixed {
|
||||
@@ -258,3 +270,33 @@ func (i *Inventory) ReleasePorts(ctx context.Context, node, module string) error
|
||||
`delete from port_assignment where node = $1 and module = $2`, record.ID, module)
|
||||
return err
|
||||
}
|
||||
|
||||
// givenOn is every machine port a module was given on this node by its `ports` setting, and which
|
||||
// module it was given to.
|
||||
func (i *Inventory) givenOn(ctx context.Context, nodeID any) (map[int]string, error) {
|
||||
rows, err := i.store.Pool().Query(ctx,
|
||||
`select module, values->'ports' from settings
|
||||
where node = $1 and jsonb_typeof(values->'ports') = 'object'`, nodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := map[int]string{}
|
||||
for rows.Next() {
|
||||
var module string
|
||||
var raw []byte
|
||||
if err := rows.Scan(&module, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var given map[string]any
|
||||
if err := json.Unmarshal(raw, &given); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, v := range given {
|
||||
if at, ok := v.(float64); ok {
|
||||
out[int(at)] = module
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
@@ -236,3 +236,24 @@ func TestUnassigningReleasesTheModulesPorts(t *testing.T) {
|
||||
t.Fatalf("port 25 is still held in the name of a module that was unassigned: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0100: a port a node was given for a module is the node's, and the mesh never hands
|
||||
// it to another.
|
||||
func TestAGivenPortIsNeverAssigned(t *testing.T) {
|
||||
inv, node := aNodeWithModules(t, "postgres", "web")
|
||||
ctx := t.Context()
|
||||
if err := inv.SetSettings(ctx, node, "postgres",
|
||||
map[string]any{catalogue.PortsSetting: map[string]any{"5432": 20000}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := inv.PortFor(ctx, node, "web", 8080, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Machine == 20000 {
|
||||
t.Fatal("a port given to postgres was assigned to web")
|
||||
}
|
||||
if _, err := inv.PortFor(ctx, node, "web", 20000, true); !errors.Is(err, ErrPortTaken) {
|
||||
t.Fatalf("a fixed port given to another module was handed over: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user