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:
@@ -213,3 +213,55 @@ func mustNotLeak(t *testing.T, err error) {
|
||||
t.Fatalf("the password is in the error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The node moved the store, and the control plane's own connection follows (novox/hq 04-ISSUES/102).
|
||||
//
|
||||
// The connection string is what genesis wrote, port and all; the port twin is what the node's
|
||||
// settings say now. The pool's configuration is the one place both meet.
|
||||
func TestThePortTwinMovesTheStoresPort(t *testing.T) {
|
||||
alone(t)
|
||||
t.Setenv(PortVariable(example), "")
|
||||
path := filepath.Join(t.TempDir(), "inventory")
|
||||
if err := os.WriteFile(path, []byte(dsn+"\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv(FileVariable(example), path)
|
||||
|
||||
opened, err := Open(t.Context(), example)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := opened.Pool().Config().ConnConfig.Port; got != 5432 {
|
||||
t.Fatalf("with nothing said, the store is on %d rather than what the file says", got)
|
||||
}
|
||||
opened.Close()
|
||||
|
||||
t.Setenv(PortVariable(example), "6852")
|
||||
opened, err = Open(t.Context(), example)
|
||||
if err != nil {
|
||||
t.Fatalf("a moved port was refused: %v", err)
|
||||
}
|
||||
defer opened.Close()
|
||||
if got := opened.Pool().Config().ConnConfig.Port; got != 6852 {
|
||||
t.Fatalf("the store is on %d, and the node put it on 6852", got)
|
||||
}
|
||||
if got := opened.Pool().Config().ConnConfig.Password; got != "s3cret-in-here" {
|
||||
t.Fatalf("moving the port changed the password to %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPortTwinThatIsNotAPortNamesItselfAndNotTheSecret(t *testing.T) {
|
||||
alone(t)
|
||||
t.Setenv(Variable(example), dsn)
|
||||
t.Setenv(PortVariable(example), "six")
|
||||
_, err := Open(t.Context(), example)
|
||||
if err == nil {
|
||||
t.Fatal("a port that is not a number was accepted")
|
||||
}
|
||||
if !strings.Contains(err.Error(), PortVariable(example)) {
|
||||
t.Errorf("the error does not name the variable: %v", err)
|
||||
}
|
||||
if strings.Contains(err.Error(), "s3cret") {
|
||||
t.Errorf("the error quotes the password: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
+37
-1
@@ -25,6 +25,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/envfile"
|
||||
)
|
||||
|
||||
// contextName is what a context may be called.
|
||||
@@ -67,6 +69,17 @@ func Variable(context string) string {
|
||||
// raises the first one.
|
||||
func FileVariable(context string) string { return Variable(context) + "_FILE" }
|
||||
|
||||
// PortVariable is the environment variable naming the PORT this machine put the store at — the
|
||||
// third twin, beside the value and the file.
|
||||
//
|
||||
// **The connection string is sealed and the port inside it is genesis's** (novox/hq 04-ISSUES/102).
|
||||
// The control plane cannot open its own store secret to move the port, and a node's settings can
|
||||
// move the store (ADR 0100: the foundation's ports are the node's). Every consumer's binding
|
||||
// followed that setting; the control plane's own connection did not, and the mesh was headless. So
|
||||
// the port is composed into the control plane's environment from the node's settings, exactly as a
|
||||
// consumer's binding is, and the connection string's own port stands only when this says nothing.
|
||||
func PortVariable(context string) string { return envfile.PortVar(Variable(context)) }
|
||||
|
||||
// Database is what a context's database is called.
|
||||
//
|
||||
// Named after the context, so that a person looking at a PostgreSQL server can see which
|
||||
@@ -85,7 +98,7 @@ func Open(ctx context.Context, name string) (*Store, error) {
|
||||
"name, so it must be lower-case letters and digits, starting with a letter", name)
|
||||
}
|
||||
|
||||
dsn, from, err := settingsFor(name)
|
||||
dsn, from, err := placed(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -169,6 +182,29 @@ func settingsFor(name string) (dsn, from string, err error) {
|
||||
return direct, Variable(name), nil
|
||||
}
|
||||
|
||||
// placed is a context's connection string with its port moved to where this machine put the
|
||||
// store, when the node's settings say so (PortVariable), and as it was otherwise.
|
||||
func placed(name string) (dsn, from string, err error) {
|
||||
dsn, from, err = settingsFor(name)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
port, err := envfile.Port(Variable(name))
|
||||
if err != nil || port == "" {
|
||||
return dsn, from, err
|
||||
}
|
||||
moved, err := envfile.WithPort(dsn, port)
|
||||
if err != nil {
|
||||
// The variable and the port are named; the connection string is not, for the same reason
|
||||
// as everywhere else in this file.
|
||||
return "", "", fmt.Errorf(
|
||||
"%s says this machine put the %s store on port %s, and the connection settings in %s "+
|
||||
"could not be read as something with a port in them: %w",
|
||||
PortVariable(name), name, port, from, err)
|
||||
}
|
||||
return moved, from + ", on port " + port + " as " + PortVariable(name) + " says", nil
|
||||
}
|
||||
|
||||
// Context is which context this store belongs to.
|
||||
func (s *Store) Context() string { return s.context }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user