Files
mesh-controller/internal/envfile/port.go
T
jschoubben b7da02e370 An address is read from the node's settings where it is used, never recorded with a port
Three readers did not follow a moved foundation port (novox/hq 04-ISSUES/102),
and each took the control-node down in its own way: the control plane's own
store and broker connections, sealed at genesis with the port inside; and every
build the mesh ever recorded, kept as `<registry>:<port>/<module>/<artifact>@…`.

The control plane cannot open its own sealed connections to move a port, and it
cannot bind the store as a consumer would — a binding mints a credential. So its
settings get a third twin: `NAME_PORT`, composed into its container from the
node's settings by a placeholder that names a seat, `${seat:mesh-store:5432}`,
and read on top of the sealed value by the store, the broker, the management API
and the bus connection. The answer is empty when the mesh has nothing to add,
so what genesis wrote stands until the node says otherwise.

A build is now recorded by digest and path — `artifact-store://<module>/<artifact>@…`
— and the store's address is composed in where a reference is used: the
declaration, the trust file, the bases a build is handed, a replay to the
catalogue. A reference recorded before this, with an address, is re-routed the
same way when the mesh built it. The trust file and every provider's address
now come from one derivation, with the node's given port over the mesh's
assignment over the manifest's number.

novox/hq 04-ISSUES/102
2026-09-23 23:26:43 +02:00

124 lines
4.9 KiB
Go

package envfile
import (
"fmt"
"net"
"os"
"regexp"
"strconv"
"strings"
)
// The port a machine put something on, said beside the value that names it.
//
// **An address written down when a port was decided does not follow the port** (novox/hq
// 04-ISSUES/102). The control plane's own store and broker connections are written at genesis —
// full connection strings, password and all — and sealed, so the mesh cannot open them to move the
// port inside. When the node's settings move that port, every consumer's binding follows and the
// control plane's own connection does not: it goes on dialling the number genesis wrote, and the
// mesh is headless.
//
// So a setting has a third twin. `NAME_FILE` says where the value is; `NAME_PORT` says which port
// this machine put the thing at, composed into the control plane's environment from the node's
// settings exactly as a consumer's binding is. The value's own port is what stands when the twin
// says nothing — a mesh whose foundation is still where genesis raised it needs no override, and
// an empty answer is the mesh saying it has nothing to add, not the mesh saying zero.
//
// Only the port. The host inside the value is the machine's own loopback, or the broker's public
// name — a fact of the genesis, not of any node's settings — and the mesh has no better opinion
// of it. What moves under ADR 0100 is the port.
// PortVar is the twin saying which port this machine put the named thing at.
func PortVar(name string) string { return name + "_PORT" }
// Port is what NAME_PORT says, checked to be a port, or "" when it says nothing.
//
// Blank counts as unset, as it does for every other variable here: a container given an empty
// string was given nothing, and refusing it as ambiguous would turn an omission into a puzzle.
func Port(name string) (string, error) {
raw := strings.TrimSpace(os.Getenv(PortVar(name)))
if raw == "" {
return "", nil
}
n, err := strconv.Atoi(raw)
if err != nil || n < 1 || n > 65535 {
return "", fmt.Errorf("%s is %q, which is not a port", PortVar(name), raw)
}
return strconv.Itoa(n), nil
}
// Placed is Value, with its port moved to where NAME_PORT says this machine put it.
func Placed(name string) (string, error) {
value, err := Value(name)
if err != nil {
return "", err
}
port, err := Port(name)
if err != nil || port == "" {
return value, err
}
placed, err := WithPort(value, port)
if err != nil {
// Where it came from is said; what it says is not. The value is a connection string with
// a password in it, and every error here is written on the assumption it will be logged.
return "", fmt.Errorf("%s names a port to move %s to, and %s could not be read as "+
"something with a port in it: %w", PortVar(name), name, name, err)
}
return placed, nil
}
// keywordPort is `port=…` in a `key=value` connection string.
var keywordPort = regexp.MustCompile(`(^|\s)port=\S*`)
// WithPort is the value with its port replaced by `port`.
//
// Three shapes, because the control plane's settings come in three: a URL
// (`scheme://[user:password@]host[:port][/…]`), a bare `host[:port]` (the broker's address) and
// a `key=value` connection string (`host=… port=…`), which is what the store's driver accepts
// beside a URL. An IPv6 host stays in its brackets. Nothing here parses the URL properly — a
// password with a character a URL parser dislikes is still a working connection string, and
// refusing it would refuse a value that has been dialling fine since genesis.
func WithPort(value, port string) (string, error) {
value = strings.TrimSpace(value)
if value == "" {
return "", fmt.Errorf("the value is empty")
}
if scheme, rest, isURL := strings.Cut(value, "://"); isURL {
end := strings.IndexAny(rest, "/?#")
authority, tail := rest, ""
if end >= 0 {
authority, tail = rest[:end], rest[end:]
}
userinfo := ""
if at := strings.LastIndex(authority, "@"); at >= 0 {
userinfo, authority = authority[:at+1], authority[at+1:]
}
host, err := hostWithPort(authority, port)
if err != nil {
return "", err
}
return scheme + "://" + userinfo + host + tail, nil
}
if strings.Contains(value, "=") && !strings.ContainsAny(value, "/") {
if keywordPort.MatchString(value) {
return keywordPort.ReplaceAllString(value, "${1}port="+port), nil
}
return value + " port=" + port, nil
}
return hostWithPort(value, port)
}
// hostWithPort is `host[:port]` with the port set, brackets kept around an IPv6 host.
func hostWithPort(authority, port string) (string, error) {
if authority == "" {
return "", fmt.Errorf("there is no host to put a port on")
}
host, _, err := net.SplitHostPort(authority)
if err != nil {
// No port yet. A bracketed IPv6 host without a port is a shape SplitHostPort refuses, and
// a bare one carries colons of its own — both are a host, not an error.
host = strings.TrimSuffix(strings.TrimPrefix(authority, "["), "]")
}
return net.JoinHostPort(host, port), nil
}