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:<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 "".
223 lines
9.0 KiB
Go
223 lines
9.0 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Telling a module where this machine put the holder of a seat.
|
|
//
|
|
// **The foundation's ports are the node's** (novox/hq ADR 0100): the port a foundation server was
|
|
// given at genesis becomes that node's setting for the module that serves it, and every reader
|
|
// follows the setting. Every consumer's binding did. The control plane's own connections did not
|
|
// (04-ISSUES/102): they are written at genesis, before any module exists to bind to — full
|
|
// connection strings, sealed, with the port inside — so when the node moved the store, the
|
|
// control plane went on dialling where genesis had written and the mesh was headless.
|
|
//
|
|
// The control plane cannot open its own sealed connection to move the port, and it cannot bind
|
|
// the store as a consumer would: a binding mints a credential, and what the control plane holds
|
|
// is the foundation's superuser, made before the mesh. What it can do is read the node's settings
|
|
// when it composes its own declaration — it is the thing that composes every other module's — and
|
|
// say in its own environment which port this machine put the store at.
|
|
//
|
|
// So a module may ask about a **seat** (ADR 0079: a foundation seat is named after the server it
|
|
// guards — `mesh-store`, `mesh-broker`). `${seat:mesh-store:5432}` is "the port this machine put
|
|
// the holder of the mesh-store seat's 5432 at". Not a provision: nothing is required, nothing is
|
|
// granted, no credential is minted. A seat is the mesh's own vocabulary for the store and the
|
|
// broker, which is what makes this the control plane's way of naming them and not a way for a
|
|
// module to reach a server it was not granted — the answer is a port number the mesh holds in the
|
|
// clear, and the credential to use it is still the module's own to have.
|
|
//
|
|
// **The answer may be empty, and that is the one place a placeholder answers with nothing.** The
|
|
// store and the broker are raised at genesis, before the mesh knows them as modules; a mesh raised
|
|
// on the catalogue's own ports never gives them a setting at all. In both, the port genesis wrote
|
|
// into the connection string is the right one, and the mesh has nothing to add. An empty answer
|
|
// says exactly that, and what reads it — the control plane's `_PORT` twin — treats an empty value
|
|
// as no value. Answering with the software's own port instead would override what genesis wrote
|
|
// with a number the mesh never checked, on the one machine where that is a headless mesh.
|
|
|
|
// ofSeat is where a module asks about a seat: ${seat:<seat>:<the port its holder's software uses>}.
|
|
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:<mesh-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.
|
|
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) && !ofSeatReach.MatchString(content)) {
|
|
return nil
|
|
}
|
|
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":
|
|
env, ok := resource["env"].(map[string]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
named := make([]string, 0, len(env))
|
|
for key := range env {
|
|
named = append(named, key)
|
|
}
|
|
sort.Strings(named)
|
|
|
|
// A fresh map, and only when something changes — this map is the catalogue's, shared by
|
|
// every node running the module (see portInto).
|
|
var filled map[string]any
|
|
for _, key := range named {
|
|
written, ok := env[key].(string)
|
|
if !ok || (!ofSeat.MatchString(written) && !ofSeatReach.MatchString(written)) {
|
|
continue
|
|
}
|
|
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 {
|
|
filled[k] = v
|
|
}
|
|
}
|
|
filled[key] = value
|
|
}
|
|
if filled != nil {
|
|
resource["env"] = filled
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// seatsFilledInto answers every ${seat:…} in one written value.
|
|
//
|
|
// A port the seat's holder does not publish on this machine — or a seat nothing on it holds —
|
|
// answers with nothing, for the reason the package comment gives. A port that is not one is
|
|
// refused: it was written by a person and it is wrong.
|
|
func seatsFilledInto(written, where string, with Rendering) (string, error) {
|
|
for _, m := range ofSeat.FindAllStringSubmatch(written, -1) {
|
|
seat, port := m[1], m[2]
|
|
wanted, err := strconv.Atoi(port)
|
|
if err != nil || wanted < 1 || wanted > 65535 {
|
|
return "", fmt.Errorf("%s says ${seat:%s:%s}, and %s is not a port", where, seat, port, port)
|
|
}
|
|
answer := ""
|
|
if at, known := with.Seats[seat][wanted]; known {
|
|
answer = strconv.Itoa(at)
|
|
}
|
|
written = strings.ReplaceAll(written, m[0], answer)
|
|
}
|
|
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
|
|
}
|