The proxy is told its routes and the mesh on the bus, and serves internal names to the mesh only
The proxy answered every routed name to any request carrying it, so an internal-only route would have been public under its internal name. Each membership now carries what its module receives, from the same composition as its received file, and every machine's private-network address, the list the packet filter's "from the mesh" is. The proxy follows its membership, serves internal names only to those machines and itself, and keeps the file until the bus has spoken (novox/hq ADR 0167, issue 191).
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
@@ -33,6 +34,17 @@ type Membership struct {
|
||||
Reaches map[string][]string `json:"reaches,omitempty"`
|
||||
// Tools is where this instance answers what it serves — the runtime's one verb of its own.
|
||||
Tools string `json:"tools"`
|
||||
// Receives is what this assignment is given for each requirement it receives, by requirement:
|
||||
// the contributions of every module that asked for it, as the catalogue composed them (novox/hq
|
||||
// ADR 0167). The same list its received file is written from, so the two cannot disagree; a
|
||||
// requirement nobody contributed to is an empty list, never absent. Kept as JSON because the
|
||||
// catalogue owns the shape of a contribution and the bus only carries it.
|
||||
Receives map[string]json.RawMessage `json:"receives,omitempty"`
|
||||
// Mesh is every machine's address on the private network — what a rule saying "from the mesh"
|
||||
// resolves to in the packet filter, issued here from the same list (novox/hq ADR 0167). A
|
||||
// module that must tell the mesh from the world, the route proxy serving an internal name, reads
|
||||
// it here rather than keeping a definition of its own.
|
||||
Mesh []string `json:"mesh,omitempty"`
|
||||
}
|
||||
|
||||
// Served is one address a tool is answered on.
|
||||
|
||||
@@ -241,15 +241,21 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) {
|
||||
// Owner is kept beside the resources because a resource id cannot be split back into its module:
|
||||
// a module's name may itself contain a dot. What the mesh adds of its own — an opening, the guard —
|
||||
// has no owner.
|
||||
//
|
||||
// Received is what each module on the machine is given for each requirement it receives — the same
|
||||
// contributions its received file is written from, kept beside it so the mesh can also issue them
|
||||
// on the bus in the module's membership (novox/hq ADR 0167). By module, then requirement.
|
||||
type Composed struct {
|
||||
Resources []map[string]any
|
||||
Owner map[string]string
|
||||
Received map[string]map[string][]Contribution
|
||||
}
|
||||
|
||||
// Compose is Declaration with the owner of every resource said.
|
||||
func (r Resolution) Compose(with Rendering) (Composed, error) {
|
||||
owner := map[string]string{}
|
||||
resources, err := r.compose(with, owner)
|
||||
received := map[string]map[string][]Contribution{}
|
||||
resources, err := r.compose(with, owner, received)
|
||||
if err != nil {
|
||||
return Composed{}, err
|
||||
}
|
||||
@@ -261,7 +267,7 @@ func (r Resolution) Compose(with Rendering) (Composed, error) {
|
||||
"sealed": with.BusMembership, "mode": "0600",
|
||||
})
|
||||
}
|
||||
return Composed{Resources: resources, Owner: owner}, nil
|
||||
return Composed{Resources: resources, Owner: owner, Received: received}, nil
|
||||
}
|
||||
|
||||
// BusMembershipID names the resource carrying a machine's membership for the new bus, and
|
||||
@@ -270,7 +276,8 @@ func BusMembershipID() string { return "bus-membership" }
|
||||
|
||||
const BusMembershipPath = "/var/lib/mesh/membership-next.json"
|
||||
|
||||
func (r Resolution) compose(with Rendering, owner map[string]string) ([]map[string]any, error) {
|
||||
func (r Resolution) compose(with Rendering, owner map[string]string,
|
||||
received map[string]map[string][]Contribution) ([]map[string]any, error) {
|
||||
// Every manifest is placed first (novox/hq ADR 0112): the maps naming where its bindings,
|
||||
// credentials and contributions land are resolved against this node's directories, so every
|
||||
// reader below — the binding files, the sealed secrets, the grant paths a contribution
|
||||
@@ -596,6 +603,12 @@ func (r Resolution) compose(with Rendering, owner map[string]string) ([]map[stri
|
||||
return nil, err
|
||||
}
|
||||
first = append(first, file)
|
||||
if received[m.Module] == nil {
|
||||
received[m.Module] = map[string][]Contribution{}
|
||||
}
|
||||
// Empty rather than absent when nobody contributed, for the reason the file is
|
||||
// written empty: "nothing asked" and "never told" want different responses.
|
||||
received[m.Module][to] = append([]Contribution{}, given[to]...)
|
||||
}
|
||||
if m.Keeps != "" && with.Kept != nil {
|
||||
file, err := keptFile(m.Keeps, with.Kept)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// What a provider receives is composed once, and issued twice: as its received file, and in its
|
||||
// membership on the bus (novox/hq ADR 0167). The two are the same list, so a proxy reading the bus
|
||||
// and one reading the file serve the same routes — including the port the machine published, which
|
||||
// is the same-node fix the file already carries.
|
||||
func TestWhatAProviderReceivesIsTheSameOnTheBusAsInItsFile(t *testing.T) {
|
||||
gitea := Manifest{
|
||||
Module: "gitea", Version: "1",
|
||||
Listens: []Listening{{Port: 3000, Protocol: "tcp", From: FromMesh}},
|
||||
Contributes: map[string]map[string]any{"route": {"label": "git", "port": 3000}},
|
||||
Resources: []map[string]any{{
|
||||
"id": "server", "type": "container", "name": "gitea", "ports": []any{"3000"},
|
||||
}},
|
||||
}
|
||||
r, err := Resolve(shelf(gitea, routeProxy(), stepCA()),
|
||||
[]string{"gitea", "route-proxy", "step-ca"}, reachable(), World{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
composed, err := r.Compose(Rendering{Ports: map[string]map[int]int{"gitea": {3000: 20000}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
file := fileNamed(composed.Resources, "route-proxy.received-route")
|
||||
if file == nil {
|
||||
t.Fatal("the proxy was given no routes file")
|
||||
}
|
||||
var written struct {
|
||||
Given []Contribution `json:"given"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(file["content"].(string)), &written); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
issued, said := composed.Received["route-proxy"]["route"]
|
||||
if !said {
|
||||
t.Fatalf("nothing is issued for the proxy to receive on the bus: %v", composed.Received)
|
||||
}
|
||||
// Compared as JSON, which is what both are once they leave the controller.
|
||||
a, _ := json.Marshal(written.Given)
|
||||
b, _ := json.Marshal(issued)
|
||||
var fromFile, fromBus any
|
||||
_ = json.Unmarshal(a, &fromFile)
|
||||
_ = json.Unmarshal(b, &fromBus)
|
||||
if !reflect.DeepEqual(fromFile, fromBus) {
|
||||
t.Errorf("the bus and the file disagree about the routes:\nfile %s\nbus %s", a, b)
|
||||
}
|
||||
if len(issued) != 1 {
|
||||
t.Fatalf("expected one route on the bus, got %v", issued)
|
||||
}
|
||||
if port, ok := asPort(issued[0].Values["port"]); !ok || port != 20000 {
|
||||
t.Errorf("the bus carries a port nothing listens on: %v", issued[0].Values["port"])
|
||||
}
|
||||
|
||||
// A module that receives nothing is issued nothing to receive.
|
||||
if _, any := composed.Received["gitea"]; any {
|
||||
t.Errorf("a module that receives nothing was issued something: %v", composed.Received["gitea"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user