From 67e291c02ac3569e85255eabf30a4a86b9ada04b Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 5 Oct 2026 22:16:23 +0200 Subject: [PATCH] Tell a module where a mesh seat's holder is reached (hq ADR 0222) The container runtime's module must state the mesh's registry to the runtime it owns, so the controller can stop writing that into the runtime's file (hq issue 190). ${seat::reach} answers host:port without a binding: nothing required, granted or minted, and the address is one the mesh already composes into every reference it built. Only mesh-artifact-store is answered; another seat is refused by name. Unanswered in a file written into as JSON, the empty member is dropped, so the runtime is never told to trust "". --- cmd/mesh-controller/plan.go | 9 ++- internal/catalogue/declaration.go | 5 ++ internal/catalogue/seat_into.go | 114 +++++++++++++++++++++++++-- internal/catalogue/seat_into_test.go | 81 +++++++++++++++++++ 4 files changed, 202 insertions(+), 7 deletions(-) diff --git a/cmd/mesh-controller/plan.go b/cmd/mesh-controller/plan.go index 2c66137..94c66f8 100644 --- a/cmd/mesh-controller/plan.go +++ b/cmd/mesh-controller/plan.go @@ -732,6 +732,13 @@ func renderingFor(ctx context.Context, open *stores, node string, if err != nil { return catalogue.Rendering{}, inventory.Node{}, err } + // Where this machine reaches each mesh seat's holder, for ${seat::reach} (novox/hq ADR + // 0222): the artifact store as this network reaches it, which the container runtime is told to + // trust (ADR 0082). The same address composed into every reference the mesh built. + var reach map[string]string + if artifactStore != "" { + reach = map[string]string{"mesh-artifact-store": artifactStore} + } return catalogue.Rendering{ BusMembership: memberships[node], Settings: settings, Generators: gens, Grants: grants, Needed: needed, Ports: ports, @@ -739,7 +746,7 @@ func renderingFor(ctx context.Context, open *stores, node string, Machines: machines, Zones: zones, Suffix: overlay.Suffix(), MeshRange: meshRange, TunnelInterface: overlay.Interface, Accounts: accounts, Foundation: foundation, Kept: kept, Adopted: record.Adopted, OutwardLinks: outwardLinks, - Given: given, Taken: taken, Seats: seats, ArtifactStore: artifactStore, Built: built, + Given: given, Taken: taken, Seats: seats, ArtifactStore: artifactStore, SeatReach: reach, Built: built, BusUsers: busUsers, }, record, nil } diff --git a/internal/catalogue/declaration.go b/internal/catalogue/declaration.go index fb64418..86204a4 100644 --- a/internal/catalogue/declaration.go +++ b/internal/catalogue/declaration.go @@ -201,6 +201,11 @@ type Rendering struct { // stored (novox/hq 04-ISSUES/102). ArtifactStore string + // SeatReach is where this machine reaches the holder of each mesh-scoped seat it may be asked + // about (host:port), by seat: what ${seat::reach} answers with (novox/hq ADR 0222). Absent + // when no holder is reachable yet; see seat_into.go for which seats are answered. + SeatReach map[string]string + // Built is every `/` the mesh has built. What tells a reference recorded // with an address — before references were kept without one — from an image a module runs // straight from a public registry. diff --git a/internal/catalogue/seat_into.go b/internal/catalogue/seat_into.go index a38909a..ea7c833 100644 --- a/internal/catalogue/seat_into.go +++ b/internal/catalogue/seat_into.go @@ -1,6 +1,7 @@ package catalogue import ( + "encoding/json" "fmt" "regexp" "sort" @@ -42,6 +43,27 @@ import ( // ofSeat is where a module asks about a seat: ${seat::}. var ofSeat = regexp.MustCompile(`\$\{seat:([a-z0-9][a-z0-9-]*):([0-9]+)\}`) +// **Where this machine reaches a mesh seat's holder** (novox/hq ADR 0222, issue 190). +// +// `${seat::reach}` is host:port — the address this machine dials to reach whatever holds a +// seat the mesh holds once. The same reasoning as the port above, one step further: nothing is +// required, nothing is granted, no credential is minted, and the answer is an address the mesh +// already holds in the clear and composes into every reference it built. What it is for is a module +// that must *state* where a mesh service is to software it owns — the container runtime trusting +// the mesh's registry is the case — without becoming that service's consumer. +// +// Answered for the seats in reachedSeats only. Another seat is refused by name rather than answered +// with nothing: a module asking where something is that the mesh does not say would otherwise be +// given an empty value and never know the question was not understood. +// +// The answer may be empty: no machine on the private network holds the seat yet (genesis raises +// the store before the network). In a file written into as JSON, an empty member is dropped from +// its list, and a list left with none is dropped, so the runtime is never told to trust "". +var ofSeatReach = regexp.MustCompile(`\$\{seat:([a-z0-9][a-z0-9-]*):reach\}`) + +// reachedSeats is every seat ${seat:…:reach} answers for. +var reachedSeats = map[string]bool{"mesh-artifact-store": true} + // seatInto replaces a resource's ${seat:…} placeholders with where this machine put each seat's // holder — in a file's content, and in a value of a container's or a process's environment. The // same places portInto fills, for the same reason: they are where a program reads a number from. @@ -49,13 +71,24 @@ func seatInto(resource map[string]any, module string, with Rendering) error { switch fmt.Sprint(resource["type"]) { case "file": content, ok := resource["content"].(string) - if !ok || !ofSeat.MatchString(content) { + if !ok || (!ofSeat.MatchString(content) && !ofSeatReach.MatchString(content)) { return nil } - filled, err := seatsFilledInto(content, fmt.Sprintf("%s has a file that", module), with) + where := fmt.Sprintf("%s has a file that", module) + filled, err := seatsFilledInto(content, where, with) if err != nil { return err } + if ofSeatReach.MatchString(filled) { + if filled, err = reachFilledInto(filled, where, with); err != nil { + return err + } + if fmt.Sprint(resource["into"]) == "json" { + if filled, err = withoutEmptyMembers(filled, where); err != nil { + return err + } + } + } resource["content"] = filled case "container", "process": @@ -74,15 +107,18 @@ func seatInto(resource map[string]any, module string, with Rendering) error { var filled map[string]any for _, key := range named { written, ok := env[key].(string) - if !ok || !ofSeat.MatchString(written) { + if !ok || (!ofSeat.MatchString(written) && !ofSeatReach.MatchString(written)) { continue } - value, err := seatsFilledInto(written, - fmt.Sprintf("%s's %s %s sets %s to something that", - module, resource["type"], resource["name"], key), with) + where := fmt.Sprintf("%s's %s %s sets %s to something that", + module, resource["type"], resource["name"], key) + value, err := seatsFilledInto(written, where, with) if err != nil { return err } + if value, err = reachFilledInto(value, where, with); err != nil { + return err + } if filled == nil { filled = map[string]any{} for k, v := range env { @@ -118,3 +154,69 @@ func seatsFilledInto(written, where string, with Rendering) (string, error) { } return written, nil } + +// reachFilledInto answers every ${seat:…:reach} in one written value with where this machine +// reaches the seat's holder, or with nothing when no holder is reachable yet. A seat the mesh does +// not answer this for is refused by name. +func reachFilledInto(written, where string, with Rendering) (string, error) { + for _, m := range ofSeatReach.FindAllStringSubmatch(written, -1) { + seat := m[1] + if !reachedSeats[seat] { + known := make([]string, 0, len(reachedSeats)) + for s := range reachedSeats { + known = append(known, s) + } + sort.Strings(known) + return "", fmt.Errorf("%s says ${seat:%s:reach}, and the mesh says where a seat's holder is "+ + "reached only for %s (novox/hq ADR 0222)", where, seat, strings.Join(known, ", ")) + } + written = strings.ReplaceAll(written, m[0], with.SeatReach[seat]) + } + return written, nil +} + +// withoutEmptyMembers drops every empty string from the lists at the top of a JSON object written +// into a machine's file, and a list left with no members, so an unanswered ${seat:…:reach} adds +// nothing to the machine's list rather than adding "". +func withoutEmptyMembers(content, where string) (string, error) { + var object map[string]json.RawMessage + if err := json.Unmarshal([]byte(content), &object); err != nil { + return "", fmt.Errorf("%s is written into as JSON and is not a JSON object: %w", where, err) + } + changed := false + for key, raw := range object { + var members []json.RawMessage + if err := json.Unmarshal(raw, &members); err != nil { + continue // not a list + } + kept := make([]json.RawMessage, 0, len(members)) + for _, member := range members { + var s string + if json.Unmarshal(member, &s) == nil && s == "" { + continue + } + kept = append(kept, member) + } + if len(kept) == len(members) { + continue + } + changed = true + if len(kept) == 0 { + delete(object, key) + continue + } + list, err := json.Marshal(kept) + if err != nil { + return "", err + } + object[key] = list + } + if !changed { + return content, nil + } + out, err := json.Marshal(object) + if err != nil { + return "", err + } + return string(out) + "\n", nil +} diff --git a/internal/catalogue/seat_into_test.go b/internal/catalogue/seat_into_test.go index 7cffab7..05aae7c 100644 --- a/internal/catalogue/seat_into_test.go +++ b/internal/catalogue/seat_into_test.go @@ -1,6 +1,7 @@ package catalogue import ( + "fmt" "os" "strings" "testing" @@ -211,3 +212,83 @@ func withSeatPorts(m Manifest) Manifest { } return out } + +// Where this machine reaches a mesh seat's holder (novox/hq ADR 0222, issue 190): the container +// runtime's module tells the runtime to trust the mesh's registry without binding the store. + +func runtimeTrust() map[string]any { + return map[string]any{ + "type": "file", "id": "daemon", "path": "/etc/docker/daemon.json", "into": "json", + "content": `{"live-restore": true, "insecure-registries": ["${seat:mesh-artifact-store:reach}"]}` + "\n", + } +} + +func TestASeatsReachIsWhereThisMachineReachesItsHolder(t *testing.T) { + file := runtimeTrust() + with := Rendering{SeatReach: map[string]string{"mesh-artifact-store": "anchor.internal:5100"}} + if err := seatInto(file, "docker", with); err != nil { + t.Fatal(err) + } + want := `{"live-restore": true, "insecure-registries": ["anchor.internal:5100"]}` + "\n" + if file["content"] != want { + t.Fatalf("content = %q, want %q", file["content"], want) + } + + process := map[string]any{"type": "process", "name": "p", + "env": map[string]any{"REGISTRY": "${seat:mesh-artifact-store:reach}"}} + if err := seatInto(process, "x", with); err != nil { + t.Fatal(err) + } + if got := process["env"].(map[string]any)["REGISTRY"]; got != "anchor.internal:5100" { + t.Fatalf("an environment value was filled with %q", got) + } +} + +// With no holder reachable, the runtime is not told to trust "": the member is dropped, and the +// list with it when nothing else is in it. +func TestAnUnansweredReachAddsNothingToAList(t *testing.T) { + file := runtimeTrust() + if err := seatInto(file, "docker", Rendering{}); err != nil { + t.Fatal(err) + } + if got := file["content"]; got != `{"live-restore":true}`+"\n" { + t.Fatalf("with no store reachable, the runtime's keys are %q", got) + } + + kept := map[string]any{"type": "file", "into": "json", + "content": `{"insecure-registries": ["${seat:mesh-artifact-store:reach}", "192.0.2.7:5000"]}`} + if err := seatInto(kept, "docker", Rendering{}); err != nil { + t.Fatal(err) + } + if got := kept["content"]; got != `{"insecure-registries":["192.0.2.7:5000"]}`+"\n" { + t.Fatalf("a list's other members were not kept: %q", got) + } +} + +func TestAReachTheMeshDoesNotAnswerIsRefused(t *testing.T) { + file := map[string]any{"type": "file", "content": "${seat:mesh-store:reach}"} + err := seatInto(file, "x", Rendering{SeatReach: map[string]string{"mesh-store": "anchor.internal:5432"}}) + if err == nil || !strings.Contains(err.Error(), "mesh-artifact-store") { + t.Fatalf("a reach the mesh does not answer was not refused by name: %v", err) + } +} + +// Through the whole composition, as the container runtime's module declares it. +func TestTheRuntimesTrustIsComposedFromTheSeatsReach(t *testing.T) { + r := Resolution{Node: "anchor", Modules: []Manifest{{ + Module: "docker", Resources: []map[string]any{runtimeTrust()}, + }}} + out, err := r.Declaration(Rendering{ + SeatReach: map[string]string{"mesh-artifact-store": "anchor.internal:5100"}, + }) + if err != nil { + t.Fatal(err) + } + file := fileNamed(out, "docker.daemon") + if file == nil { + t.Fatalf("no file in %v", out) + } + if got := file["content"]; !strings.Contains(fmt.Sprint(got), `["anchor.internal:5100"]`) { + t.Fatalf("the runtime's file says %q", got) + } +}