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`, read on top of the sealed value by the
store, the broker, the management API and the bus connection, and filled into
its container by a placeholder that names a seat, `${seat:mesh-store:5432}`,
from the node's given or mesh-assigned ports — never the manifest's number, and
empty when the mesh has nothing to add, so what genesis wrote stands. A value
that is still a placeholder is nothing said, aloud: the manifest naming it lands
in the next commit, once every control plane that composes it knows it.
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. Over the network as `<node>.internal:<port>`; on the store's own node
before any network exists — every genesis push before its "network" step — by
loopback. 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
come from one derivation: the node's given port, over the mesh's assignment,
over the manifest's number.
novox/hq 04-ISSUES/102
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
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
|
||||
}
|
||||
if unfilled.MatchString(raw) {
|
||||
// **A placeholder the mesh never filled, and this is the one place it must not be a
|
||||
// fault.** The control plane composes its own declaration, so a manifest naming
|
||||
// `${seat:…}` reaches a control plane one build older than the manifest — one that does
|
||||
// not know the placeholder and passes it through as the value. Refusing it here would
|
||||
// leave every command and the daemon unable to open a store: headless, on the machine
|
||||
// that cannot be repaired through the mesh (novox/hq 04-ISSUES/102). So it is what an
|
||||
// empty answer is — nothing said, the sealed value stands — and it is said aloud, because
|
||||
// a manifest ahead of its binary is worth a line on stderr and not worth an outage.
|
||||
fmt.Fprintf(os.Stderr, "%s is %q, a placeholder nothing filled in — ignored; the port in "+
|
||||
"%s stands. The control plane composing this declaration is older than the manifest "+
|
||||
"naming it: rebuild and push it\n", PortVar(name), raw, name)
|
||||
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*`)
|
||||
|
||||
// unfilled is a value that is still a placeholder — `${…}` — rather than something a person or the
|
||||
// mesh wrote as a port.
|
||||
var unfilled = regexp.MustCompile(`^\$\{[^}]*\}$`)
|
||||
|
||||
// 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 {
|
||||
// **The password may hold any of `/ ? # @`, so the host begins after the LAST `@`**, and
|
||||
// the path only after that. Cutting at the first `/` would take a password's slash for the
|
||||
// path's and put the new port on the user name — a connection string that dials the wrong
|
||||
// host without a word. Genesis makes passwords that cannot do this; an accepted one can.
|
||||
// The connection strings this reads — a store's, a broker's, a management API's — carry
|
||||
// no `@` after their userinfo, which is what makes the last one the boundary.
|
||||
userinfo, hostAndTail := "", rest
|
||||
if at := strings.LastIndex(rest, "@"); at >= 0 {
|
||||
userinfo, hostAndTail = rest[:at+1], rest[at+1:]
|
||||
}
|
||||
authority, tail := hostAndTail, ""
|
||||
if end := strings.IndexAny(hostAndTail, "/?#"); end >= 0 {
|
||||
authority, tail = hostAndTail[:end], hostAndTail[end:]
|
||||
}
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user