filtering: a per-node expose setting overrides a listen's source (ADR 0051)
#4
@@ -138,8 +138,19 @@ 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, with.Ports)
|
||||
// would write a rule set that closed every other module on the machine. Each module's per-node
|
||||
// exposure settings override its listens' source first (novox/hq ADR 0051).
|
||||
exposure := map[string]map[int]string{}
|
||||
for _, m := range r.Modules {
|
||||
e, err := Exposure(m, with.Settings[m.Module])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e != nil {
|
||||
exposure[m.Module] = e
|
||||
}
|
||||
}
|
||||
rules, err := r.Filtering(with.Generators, with.Ports, exposure)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package catalogue
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -31,7 +32,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, ports map[string]map[int]int) ([]Rule, error) {
|
||||
func (r Resolution) Filtering(computed map[string]Generator, ports map[string]map[int]int, exposure map[string]map[int]string) ([]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.
|
||||
@@ -70,10 +71,17 @@ func (r Resolution) Filtering(computed map[string]Generator, ports map[string]ma
|
||||
if at, known := ports[m.Module][l.Port]; known {
|
||||
port = at
|
||||
}
|
||||
at := opening{port: port, protocol: l.At(), from: l.From}
|
||||
// The source, with any per-node exposure setting applied. The override names the port
|
||||
// the module declares, so it travels with that port to wherever the machine publishes
|
||||
// it (novox/hq ADR 0051): the same module is internal on one node and public on another.
|
||||
from := l.From
|
||||
if override, set := exposure[m.Module][l.Port]; set {
|
||||
from = override
|
||||
}
|
||||
at := opening{port: port, protocol: l.At(), from: from}
|
||||
rule, seen := found[at]
|
||||
if !seen {
|
||||
rule = &Rule{Port: port, Protocol: l.At(), From: l.From}
|
||||
rule = &Rule{Port: port, Protocol: l.At(), From: from}
|
||||
found[at] = rule
|
||||
}
|
||||
rule.Because = append(rule.Because, m.Module)
|
||||
@@ -100,6 +108,62 @@ func (r Resolution) Filtering(computed map[string]Generator, ports map[string]ma
|
||||
return widest(out), nil
|
||||
}
|
||||
|
||||
// ExposeSetting is the settings key that overrides a listen's source per node (novox/hq ADR 0051):
|
||||
//
|
||||
// {"expose": {"5432": "anywhere"}}
|
||||
//
|
||||
// makes the module's port 5432 open to the public on the node this is set for, whatever the
|
||||
// manifest's default. It keys on the port the module declares — the mesh maps that to where the
|
||||
// machine publishes, and the override travels with it.
|
||||
const ExposeSetting = "expose"
|
||||
|
||||
// Exposure reads a module's per-node exposure overrides from its settings: declared-port → source.
|
||||
//
|
||||
// It refuses an override for a port the module does not listen on, or to a source that is not a
|
||||
// real one — an exposure setting that reaches no port, or names a source nothing enforces, is the
|
||||
// "reads as a restriction and is none" fault this whole mechanism exists to prevent (novox/hq
|
||||
// ADR 0048/0050). A module with no `expose` setting yields nothing and keeps its manifest defaults.
|
||||
func Exposure(m Manifest, layers []Layer) (map[int]string, error) {
|
||||
listened := make(map[int]bool, len(m.Listens))
|
||||
for _, l := range m.Listens {
|
||||
listened[l.Port] = true
|
||||
}
|
||||
|
||||
out := map[int]string{}
|
||||
for _, layer := range layers {
|
||||
raw, ok := layer.Values[ExposeSetting]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
entries, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s: %s is a { port: source } map, and %q set it to something else",
|
||||
m.Module, ExposeSetting, layer.From)
|
||||
}
|
||||
for portText, value := range entries {
|
||||
port, err := strconv.Atoi(portText)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s exposes %q, which is not a port", m.Module, portText)
|
||||
}
|
||||
if !listened[port] {
|
||||
return nil, fmt.Errorf(
|
||||
"%s exposes port %d, which it does not listen on — the setting reaches nothing",
|
||||
m.Module, port)
|
||||
}
|
||||
source, ok := value.(string)
|
||||
if !ok || (source != FromMesh && source != FromEverywhere && source != FromMachine) {
|
||||
return nil, fmt.Errorf("%s exposes port %d as %v; it is %q, %q or %q",
|
||||
m.Module, port, value, FromMesh, FromEverywhere, FromMachine)
|
||||
}
|
||||
out[port] = source
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// widest drops a rule that another already covers.
|
||||
//
|
||||
// A port open to anywhere is not additionally restricted by a second rule opening it to the mesh:
|
||||
|
||||
@@ -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, nil)
|
||||
rules, err := r.Filtering(computed, nil, 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{}}, nil)
|
||||
}.Filtering(map[string]Generator{"mesh-network": cannotSay{}}, nil, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a machine whose open ports could not be computed was given a rule set anyway")
|
||||
}
|
||||
@@ -578,3 +578,47 @@ func named(r Resolution) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// A port's source is a per-node setting, not a manifest constant: the same module is internal on
|
||||
// one machine and public on another (novox/hq ADR 0051). postgres listens from the mesh by default;
|
||||
// a setting on one node exposes it to anywhere, and the rule set follows.
|
||||
func TestExposureSettingOverridesAListensSource(t *testing.T) {
|
||||
postgres := Manifest{Module: "postgres", Version: "1",
|
||||
Listens: []Listening{{Port: 5432, From: FromMesh, Why: "the database"}}}
|
||||
|
||||
base, err := Resolution{Node: "ace", Modules: []Manifest{postgres}}.Filtering(nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("filtering: %v", err)
|
||||
}
|
||||
if len(base) != 1 || base[0].From != FromMesh {
|
||||
t.Fatalf("without a setting, the default source is the mesh: %+v", base)
|
||||
}
|
||||
|
||||
expose := map[string]map[int]string{"postgres": {5432: FromEverywhere}}
|
||||
exposed, err := Resolution{Node: "novox", Modules: []Manifest{postgres}}.Filtering(nil, nil, expose)
|
||||
if err != nil {
|
||||
t.Fatalf("filtering: %v", err)
|
||||
}
|
||||
if len(exposed) != 1 || exposed[0].From != FromEverywhere {
|
||||
t.Fatalf("the setting did not open the port to anywhere: %+v", exposed)
|
||||
}
|
||||
}
|
||||
|
||||
// Exposure refuses a setting that names a port the module does not listen on, or a source that is
|
||||
// not a real one — a setting reaching nothing is worse than none (novox/hq ADR 0048/0051).
|
||||
func TestExposureRefusesAPortNotListenedOnAndABadSource(t *testing.T) {
|
||||
postgres := Manifest{Module: "postgres", Listens: []Listening{{Port: 5432, From: FromMesh}}}
|
||||
layer := func(port, source string) []Layer {
|
||||
return []Layer{{From: "node", Values: map[string]any{ExposeSetting: map[string]any{port: source}}}}
|
||||
}
|
||||
|
||||
if _, err := Exposure(postgres, layer("5432", FromEverywhere)); err != nil {
|
||||
t.Fatalf("a valid exposure was refused: %v", err)
|
||||
}
|
||||
if _, err := Exposure(postgres, layer("6379", FromEverywhere)); err == nil {
|
||||
t.Error("a port the module does not listen on was accepted")
|
||||
}
|
||||
if _, err := Exposure(postgres, layer("5432", "everyone")); err == nil {
|
||||
t.Error("a source that is not mesh/anywhere/machine was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +171,11 @@ func UnusedSettings(m Manifest, layers []Layer) []string {
|
||||
var unused []string
|
||||
for _, layer := range layers {
|
||||
for key := range layer.Values {
|
||||
// `expose` is a real destination for a module that listens: it overrides a port's
|
||||
// source (novox/hq ADR 0051), validated in Exposure, so it is not stray here.
|
||||
if key == ExposeSetting && len(m.Listens) > 0 {
|
||||
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))
|
||||
|
||||
Reference in New Issue
Block a user