Declare openings and a refusal-only guard on an adopted node in place of the filter (hq ADR 0100)

This commit is contained in:
2026-09-22 17:21:44 +02:00
parent a86a6c2974
commit c3b1617693
6 changed files with 526 additions and 13 deletions
+57
View File
@@ -128,6 +128,11 @@ type Rendering struct {
// 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
// Adopted says the node is adopted (novox/hq ADR 0100): the firewall found on it stays in
// 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
}
// machinePort is where a module's port lives on this machine, or the port itself when the mesh has
@@ -257,6 +262,13 @@ func (r Resolution) compose(with Rendering, owner map[string]string) ([]map[stri
var out []map[string]any
for _, m := range r.Modules {
if with.Adopted && m.Filtering != nil {
// Nothing of a module that loads a filter, on an adopted node: its table would drop
// by default and hold accepts, and the found firewall stays in force. Every resource,
// not only the rule set — its service must not run, and a node returned to adopted
// stops it by the ordinary removal of what is no longer declared.
continue
}
resources := m.Resources
// What the mesh computes for this module goes FIRST, before the module's own resources.
@@ -567,9 +579,54 @@ func (r Resolution) compose(with Rendering, owner map[string]string) ([]map[stri
out = append(out, fact)
}
}
if with.Adopted {
// First, before anything a module declares: what the mesh needs reachable, then its guard.
// The order a machine applies is the order written here.
ours := Openings(rules, with.Foundation, Published(out))
ours = append(ours, GuardResources(r.guarded(out, owner, with))...)
out = append(ours, out...)
}
return out, nil
}
// guarded is the machine ports of every guarded port of the modules here: where each module's
// container publishes it, as composed — or where the machine put it when no container does.
func (r Resolution) guarded(out []map[string]any, owner map[string]string, with Rendering) []int {
seen := map[int]bool{}
var ports []int
for _, m := range r.Modules {
for _, want := range m.Guards {
at := with.machinePort(m.Module, want)
for _, resource := range out {
if owner[fmt.Sprint(resource["id"])] != m.Module ||
fmt.Sprint(resource["type"]) != "container" {
continue
}
listed, _ := resource["ports"].([]any)
for _, entry := range listed {
parts := strings.Split(strings.TrimSpace(fmt.Sprint(entry)), ":")
if len(parts) < 2 {
continue
}
inner, err := strconv.Atoi(strings.SplitN(parts[len(parts)-1], "/", 2)[0])
if err != nil || inner != want {
continue
}
if outer, err := strconv.Atoi(parts[len(parts)-2]); err == nil {
at = outer
}
}
}
if !seen[at] {
seen[at] = true
ports = append(ports, at)
}
}
}
sort.Ints(ports)
return ports
}
// Contribution is one module telling the answer to a requirement what it needs from it.
type Contribution struct {
// From is the module that said it, so the provider and a person reading the file can tell