The controller is to be declared as a Go bundle run by a process instead of
an image (novox/hq issue 213, ADR 0188 §1, §3). The composer could not
express that honestly yet:
- a module declaring tools had every bundle served by the node's runtime,
so the controller's own binary would have been launched a second time as
an MCP child; a bundle one of the module's resources runs is now served
only when it says `loads`
- a module's accounts went after the mesh-computed files, so secrets owned
by the account a process runs as were refused on the first apply; a
module's `user` resources now go first
- `prepares` derived its step only from a container; a process is now
prepared by the same program with `prepare` as a run-once process
- `${seat:…}` was filled only into a container's environment
- a process may say what it `replaces` (a resource of its module it no
longer declares), prefixed as the host records it, so the host keeps the
old one running until the process is (needs mesh-host's `replaces`)
This lands before the controller's manifest uses any of it: the running
controller composes its own declaration, so the code that fills the new
shape must be live first.
123 lines
5.2 KiB
Go
123 lines
5.2 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"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]+)\}`)
|
|
|
|
// 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 two places
|
|
// portInto fills, for the same reason: they are where a process 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) {
|
|
return nil
|
|
}
|
|
filled, err := seatsFilledInto(content, fmt.Sprintf("%s has a file that", module), with)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resource["content"] = filled
|
|
|
|
// A process's environment as a container's (novox/hq issue 213): the controller reads where its
|
|
// store and broker are from it, whichever way the host runs it.
|
|
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) {
|
|
continue
|
|
}
|
|
value, err := seatsFilledInto(written,
|
|
fmt.Sprintf("%s's %s %s sets %s to something that",
|
|
module, resource["type"], resource["name"], key), with)
|
|
if 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
|
|
}
|