The mesh assigns the port, and a module says it once
novox/hq ADR 0038. A module cannot choose a port: it is written once and assigned anywhere, so any number it picks is a guess about a machine it has never seen. A database module met the mesh's own store on 5432 and was told, by a container runtime three layers down, that the port was already allocated. The number used to appear three times in every module — the rule set, what a consumer is told, and what the runtime publishes — agreeing only because one person wrote all three. Now it appears once, in `listens`, and the other two are derived: the container publishes `20000:5432`, the consumer is told 20000, and the rule set opens 20000. An assignment is made once and kept, as a credential is. A port that moved on every declaration would restart both ends each time and hand a consumer a number that was true when it was read. Ports the protocol fixes — mail on 25, submission on 587, DNS on 53 — say so, and are then claims: one holder per machine, and the second is refused by name at assignment. That is the mechanism the mesh already has for what is singular on a machine, pointed at ports. A mapping written the long way is left exactly as it is. Some things must be pinned by hand, and quietly overruling somebody who wrote both halves would be worse than not offering the short form. Still open, and known: the substrate is not a module, so the mesh has never heard of its own store and cannot yet assign around it. That is what 028 will still be about after this.
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -97,6 +98,24 @@ type Rendering struct {
|
||||
// rather than resolved, because who consumes a node is a fact about the rest of the mesh and
|
||||
// resolution answers questions about one machine.
|
||||
Grants []Grant
|
||||
|
||||
// Ports is where this machine puts what each module needs reachable, by module and by the
|
||||
// port the software itself uses (novox/hq ADR 0038).
|
||||
//
|
||||
// **The one place the number now lives.** A module used to write it three times — for the rule
|
||||
// set, for what a consumer is told, and for what the runtime publishes — and nothing checked
|
||||
// that the three agreed. They are all derived from this.
|
||||
Ports 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, known := r.Ports[module][wanted]; known {
|
||||
return at
|
||||
}
|
||||
return wanted
|
||||
}
|
||||
|
||||
// Declaration is everything the resolved modules put on the node, with settings applied.
|
||||
@@ -120,7 +139,7 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) {
|
||||
|
||||
// Once, from every module's listens -- not per module. A module receiving only its own ports
|
||||
// would write a rule set that closed every other module on the machine.
|
||||
rules, err := r.Filtering(with.Generators)
|
||||
rules, err := r.Filtering(with.Generators, with.Ports)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -345,6 +364,7 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) {
|
||||
if err := pinned(copied, m.Module); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
publishedOn(copied, m.Module, with)
|
||||
copied["id"] = m.Module + "." + fmt.Sprint(resource["id"])
|
||||
// A service saying what it reflects names resources within its own module, so those
|
||||
// are prefixed too or they would point at nothing.
|
||||
@@ -593,7 +613,8 @@ func (r Resolution) ContributionsFrom(requirement, module string, settings Setti
|
||||
// nothing resolves would be worse than naming it by an address that always works.
|
||||
func here(r Resolution, requirement string) *Needed {
|
||||
for _, m := range r.Modules {
|
||||
serves, said := m.Serves[requirement]
|
||||
_, said := m.Serves[requirement]
|
||||
serves := ServedOn(m, requirement, nil)
|
||||
if !said || len(serves) == 0 {
|
||||
continue
|
||||
}
|
||||
@@ -667,3 +688,90 @@ func pinned(resource map[string]any, module string) error {
|
||||
"image — it would be fetched and fail there. Resolve the tag to a digest first",
|
||||
module, resource["id"])
|
||||
}
|
||||
|
||||
// publishedOn puts the machine's own port on the outside of a container's mapping.
|
||||
//
|
||||
// **A module writes the port the software uses; the mesh says where the machine puts it**
|
||||
// (novox/hq ADR 0038). So `"5432"` means *publish what the software calls 5432*, and this fills in
|
||||
// the half only the mesh can know.
|
||||
//
|
||||
// A mapping written the long way — `"5433:5432"` — is left exactly as it is. Some things genuinely
|
||||
// must be pinned down by hand, and quietly overruling somebody who wrote both halves would be
|
||||
// worse than not offering the short form at all.
|
||||
func publishedOn(resource map[string]any, module string, with Rendering) {
|
||||
if fmt.Sprint(resource["type"]) != "container" {
|
||||
return
|
||||
}
|
||||
listed, ok := resource["ports"].([]any)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
out := make([]any, 0, len(listed))
|
||||
for _, entry := range listed {
|
||||
written := fmt.Sprint(entry)
|
||||
if strings.Contains(written, ":") {
|
||||
out = append(out, written)
|
||||
continue
|
||||
}
|
||||
wanted, err := strconv.Atoi(strings.TrimSpace(written))
|
||||
if err != nil {
|
||||
// Not a port at all. Passed through, so the host refuses it with its own words rather
|
||||
// than this quietly dropping something somebody meant.
|
||||
out = append(out, written)
|
||||
continue
|
||||
}
|
||||
out = append(out, fmt.Sprintf("%d:%d", with.machinePort(module, wanted), wanted))
|
||||
}
|
||||
resource["ports"] = out
|
||||
}
|
||||
|
||||
// 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
|
||||
// times — for the rule set, for what a consumer is told, and for what the runtime publishes — and
|
||||
// nothing checked that the three agreed. This is what fills the second in.
|
||||
//
|
||||
// The assignment wins when there is one, and the declared port stands in when there is not: a
|
||||
// caller composing without a store still gets something coherent, and a mesh that has assigned one
|
||||
// tells the truth about where it put it.
|
||||
//
|
||||
// Only when the module offers exactly one port. A module offering several has not said which
|
||||
// belongs to which provision, and guessing would give a consumer a port that answers something
|
||||
// else — so it keeps whatever the manifest said, which may be nothing.
|
||||
func ServedOn(m Manifest, provision string, ports map[int]int) map[string]any {
|
||||
serves := m.Serves[provision]
|
||||
|
||||
out := make(map[string]any, len(serves)+1)
|
||||
for k, v := range serves {
|
||||
out[k] = v
|
||||
}
|
||||
if _, said := out["port"]; said {
|
||||
// Written by hand. Redirected to wherever the machine put it, and otherwise left alone.
|
||||
if number, ok := asPort(out["port"]); ok {
|
||||
if at, known := ports[number]; known {
|
||||
out["port"] = at
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
if len(m.Listens) != 1 {
|
||||
return out
|
||||
}
|
||||
wanted := m.Listens[0].Port
|
||||
if at, known := ports[wanted]; known {
|
||||
out["port"] = at
|
||||
} else {
|
||||
out["port"] = wanted
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func asPort(v any) (int, bool) {
|
||||
switch n := v.(type) {
|
||||
case int:
|
||||
return n, true
|
||||
case float64:
|
||||
return int(n), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ type Rule struct {
|
||||
// a consequence of what runs on it, not a second list kept in step by hand. Nothing else opens a
|
||||
// port: **what is not declared is closed**, which is the property that makes the derivation worth
|
||||
// having rather than merely tidy.
|
||||
func (r Resolution) Filtering(computed map[string]Generator) ([]Rule, error) {
|
||||
func (r Resolution) Filtering(computed map[string]Generator, ports map[string]map[int]int) ([]Rule, error) {
|
||||
// Keyed by what actually distinguishes an opening. Two modules wanting :443 from the mesh is
|
||||
// one rule with two sources; one wanting it from the mesh and another from anywhere is two,
|
||||
// and they are collapsed below -- deliberately, and only in the widening direction.
|
||||
@@ -62,10 +62,18 @@ func (r Resolution) Filtering(computed map[string]Generator) ([]Rule, error) {
|
||||
}
|
||||
}
|
||||
for _, l := range opens {
|
||||
at := opening{port: l.Port, protocol: l.At(), from: l.From}
|
||||
// **The port the machine actually publishes on** (novox/hq ADR 0038). A module says
|
||||
// the port its software uses; the mesh chooses where the machine puts it. A rule
|
||||
// naming the first would open a port nothing listens on and leave the real one shut,
|
||||
// which is a firewall that reports success and blocks the service.
|
||||
port := l.Port
|
||||
if at, known := ports[m.Module][l.Port]; known {
|
||||
port = at
|
||||
}
|
||||
at := opening{port: port, protocol: l.At(), from: l.From}
|
||||
rule, seen := found[at]
|
||||
if !seen {
|
||||
rule = &Rule{Port: l.Port, Protocol: l.At(), From: l.From}
|
||||
rule = &Rule{Port: port, Protocol: l.At(), From: l.From}
|
||||
found[at] = rule
|
||||
}
|
||||
rule.Because = append(rule.Because, m.Module)
|
||||
|
||||
@@ -369,7 +369,7 @@ func TestAMachineOffTheNetworkIsBoundToItselfByAnAddressThatWorks(t *testing.T)
|
||||
// mustFilter is the rule set, refusing to continue if it could not be computed.
|
||||
func mustFilter(t *testing.T, r Resolution, computed map[string]Generator) []Rule {
|
||||
t.Helper()
|
||||
rules, err := r.Filtering(computed)
|
||||
rules, err := r.Filtering(computed, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("no rule set could be computed: %v", err)
|
||||
}
|
||||
@@ -440,7 +440,7 @@ func TestAComputedModulesOwnListensAreNotLost(t *testing.T) {
|
||||
func TestAGeneratorThatCannotSayRefusesTheRuleSet(t *testing.T) {
|
||||
_, err := Resolution{Node: "anchor",
|
||||
Modules: []Manifest{{Module: "networking", Computed: "mesh-network"}},
|
||||
}.Filtering(map[string]Generator{"mesh-network": cannotSay{}})
|
||||
}.Filtering(map[string]Generator{"mesh-network": cannotSay{}}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a machine whose open ports could not be computed was given a rule set anyway")
|
||||
}
|
||||
|
||||
@@ -318,6 +318,16 @@ type Listening struct {
|
||||
// From is who may reach it. Required, because a rule with no source is open and must say so
|
||||
// rather than appear to restrict something.
|
||||
From string `json:"from"`
|
||||
// Fixed means the protocol chose this number, so the machine must use it too.
|
||||
//
|
||||
// **The exception, and it is a real one** (novox/hq ADR 0038). Mail is 25, submission is 587,
|
||||
// IMAP over TLS is 993 — a mail system on a port the mesh picked is a mail system nothing can
|
||||
// deliver to. Everything else the mesh assigns, because a module cannot know what else is on
|
||||
// the machine it lands on.
|
||||
//
|
||||
// A fixed port is a **claim**: one holder per machine, and the second is refused by name when
|
||||
// it is assigned rather than by a container runtime when it is applied.
|
||||
Fixed bool `json:"fixed,omitempty"`
|
||||
// Why this port is open, for somebody reading a generated rule set and wondering.
|
||||
Why string `json:"why,omitempty"`
|
||||
}
|
||||
|
||||
@@ -474,8 +474,10 @@ func servedHere(catalogue map[string]Manifest, chosen map[string]bool, want stri
|
||||
if !chosen[name] {
|
||||
continue
|
||||
}
|
||||
if serves, ok := m.Serves[want]; ok {
|
||||
return serves
|
||||
if _, ok := m.Serves[want]; ok {
|
||||
// Without assignments: this is resolution, which runs before a machine's ports are
|
||||
// known. The declaration fills the machine's own in afterwards, where it has them.
|
||||
return ServedOn(m, want, nil)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user