Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdd3638312 | ||
|
|
e07b56ce43 |
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/licences"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
@@ -153,7 +154,7 @@ func TestTheControlPlaneIsToldWhereTheNodePutTheStoreAndTheBroker(t *testing.T)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
control, err := m.Resolve([]catalogue.Built{{Name: "server", Kind: catalogue.ArtifactImage,
|
||||
control, err := withSeatPorts(m).Resolve([]catalogue.Built{{Name: "server", Kind: catalogue.ArtifactImage,
|
||||
Reference: "registry.example/control@" + aDigest}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -202,11 +203,133 @@ func TestTheControlPlaneIsToldWhereTheNodePutTheStoreAndTheBroker(t *testing.T)
|
||||
"MESH_STORE_IDENTITY_PORT": "6852",
|
||||
"MESH_STORE_LICENCES_PORT": "6852",
|
||||
"MESH_BROKER_AMQP_PORT": "5679",
|
||||
"MESH_BROKER_ADDRESS_PORT": "5671",
|
||||
"MESH_BROKER_MANAGEMENT_PORT": "15672",
|
||||
// Neither given nor assigned by the mesh: the manifest's own number is NOT the answer,
|
||||
// because the sealed value beside it carries the port genesis wrote (finding F3).
|
||||
"MESH_BROKER_ADDRESS_PORT": "",
|
||||
"MESH_BROKER_MANAGEMENT_PORT": "",
|
||||
} {
|
||||
if env[key] != want {
|
||||
t.Errorf("the control plane is told %s=%v; the node says %q", key, env[key], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// withSeatPorts is the control plane's manifest with the seat placeholders in its environment —
|
||||
// added here until module.json carries them (the manifest lands one commit after the code that
|
||||
// fills it, so a control plane one build behind never sees a placeholder it cannot fill).
|
||||
func withSeatPorts(m catalogue.Manifest) catalogue.Manifest {
|
||||
seatPorts := map[string]string{
|
||||
"MESH_STORE_INVENTORY_PORT": "${seat:mesh-store:5432}",
|
||||
"MESH_STORE_IDENTITY_PORT": "${seat:mesh-store:5432}",
|
||||
"MESH_STORE_LICENCES_PORT": "${seat:mesh-store:5432}",
|
||||
"MESH_BROKER_AMQP_PORT": "${seat:mesh-broker:5672}",
|
||||
"MESH_BROKER_MANAGEMENT_PORT": "${seat:mesh-broker:15672}",
|
||||
"MESH_BROKER_ADDRESS_PORT": "${seat:mesh-broker:5671}",
|
||||
}
|
||||
out := m
|
||||
out.Resources = nil
|
||||
for _, r := range m.Resources {
|
||||
if r["type"] != "container" {
|
||||
out.Resources = append(out.Resources, r)
|
||||
continue
|
||||
}
|
||||
copied := map[string]any{}
|
||||
for k, v := range r {
|
||||
copied[k] = v
|
||||
}
|
||||
env := map[string]any{}
|
||||
if had, ok := r["env"].(map[string]any); ok {
|
||||
for k, v := range had {
|
||||
env[k] = v
|
||||
}
|
||||
}
|
||||
for k, v := range seatPorts {
|
||||
if _, said := env[k]; !said {
|
||||
env[k] = v
|
||||
}
|
||||
}
|
||||
copied["env"] = env
|
||||
out.Resources = append(out.Resources, copied)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// aLoneNode is one capable machine with nothing placed on any network — the control-node during
|
||||
// genesis, before the "network" step, which is after the store, the broker, the vault and the
|
||||
// catalogue have each been built and pushed (finding F2).
|
||||
func aLoneNode(t *testing.T) *stores {
|
||||
t.Helper()
|
||||
inventory.ForTest(t)
|
||||
licences.ForTest(t)
|
||||
open, err := openStores(t.Context())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(open.Close)
|
||||
for _, m := range provided {
|
||||
if err := open.inventory.Provide(t.Context(), m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
record, err := open.inventory.AddNode(t.Context(), "anchor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reported, _ := json.Marshal(map[string]any{"capabilities": []map[string]any{
|
||||
{"name": "container-runtime", "present": true}}})
|
||||
var profile map[string]any
|
||||
_ = json.Unmarshal(reported, &profile)
|
||||
if err := open.inventory.RecordProfile(t.Context(), record.ID, profile); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := open.inventory.RecordSealingKey(t.Context(), record.ID, aPublicKey(t)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return open
|
||||
}
|
||||
|
||||
// **Before the network exists, the store's own node reaches it by loopback** — never refused,
|
||||
// never handed the scheme: a genesis pushes the store, the broker, the vault and the catalogue to
|
||||
// a node on no network, and builds the catalogue on a base it must be able to pull.
|
||||
func TestOnANodeWithNoNetworkTheStoreIsReachedByLoopback(t *testing.T) {
|
||||
open := aLoneNode(t)
|
||||
ctx := t.Context()
|
||||
register(t, open, aStore())
|
||||
register(t, open, catalogue.Manifest{Module: "builder", Version: "1",
|
||||
Requires: []string{catalogue.ArtifactStoreProvision},
|
||||
Resources: []map[string]any{{"id": "server", "type": "container", "name": "mesh-builder",
|
||||
"image": "registry.example/mesh-builder@" + aDigest}}})
|
||||
if err := open.inventory.RecordBuild(ctx, inventory.Build{
|
||||
ID: "b1", Repository: "r", Module: "postgres", Commit: "abc",
|
||||
Made: []inventory.Artifact{{Name: "runtime", Kind: "image",
|
||||
Reference: catalogue.ArtifactStoreScheme + "postgres/runtime@" + aDigest}},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
register(t, open, catalogue.Manifest{Module: "postgres", Version: "1",
|
||||
Resources: []map[string]any{{"id": "runtime", "type": "container", "name": "mesh-postgres",
|
||||
"image": catalogue.ArtifactStoreScheme + "postgres/runtime@" + aDigest}}})
|
||||
for _, module := range []string{"distribution", "builder", "postgres"} {
|
||||
if _, err := assign(ctx, open, "anchor", module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := open.inventory.SetSettings(ctx, "anchor", "distribution",
|
||||
map[string]any{catalogue.PortsSetting: map[string]any{"5000": 5100}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var image any
|
||||
for _, r := range composed(t, open, "anchor").Resources {
|
||||
if r["id"] == "postgres.runtime" {
|
||||
image = r["image"]
|
||||
}
|
||||
}
|
||||
if image != "127.0.0.1:5100/postgres/runtime@"+aDigest {
|
||||
t.Fatalf("on the store's own node, off any network, the image is fetched as %v", image)
|
||||
}
|
||||
held := heldBy(ctx)
|
||||
if got := held["postgres/runtime"]; got != "127.0.0.1:5100/postgres/runtime@"+aDigest {
|
||||
t.Fatalf("a builder beside the store is handed the base %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,7 +510,7 @@ func heldBy(ctx context.Context) map[string]string {
|
||||
fmt.Fprintf(os.Stderr, "could not read what this mesh has built: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
address, err := whereTheStoreIs(ctx, open.inventory)
|
||||
address, err := whereABuilderReachesTheStore(ctx, open.inventory)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "could not find the artifact store on this mesh's network, so a "+
|
||||
"module naming a base will be handed a reference nothing can fetch: %v\n", err)
|
||||
|
||||
@@ -494,11 +494,18 @@ func artifactStoreOnNetwork(ctx context.Context, inv *inventory.Inventory,
|
||||
return "", "", false, nil
|
||||
}
|
||||
|
||||
// artifactStoreAddress is the artifact store as this network reaches it, `<node>.internal:<port>`,
|
||||
// or "" when the mesh has none on its network yet — the address composed into every reference
|
||||
// the mesh built, at the moment it is used and never before (novox/hq 04-ISSUES/102).
|
||||
// artifactStoreAddress is the artifact store as `forNode` reaches it: `<node>.internal:<port>`
|
||||
// over the private network, or — when nothing is on the network yet — `127.0.0.1:<port>` for the
|
||||
// node that holds the store itself, and "" for any other. The address composed into every
|
||||
// reference the mesh built, at the moment it is used and never before (novox/hq 04-ISSUES/102).
|
||||
//
|
||||
// **Genesis places the network after the store, the broker, the vault and the catalogue.** Each
|
||||
// of those is built and pushed to a node that is on no network, and the store is on that same
|
||||
// node; an answer of "no store" there would refuse every one of those pushes and hand every one
|
||||
// of those builds a base nothing can pull. Loopback is the truth on that machine, and it is the
|
||||
// address genesis itself reaches the store by.
|
||||
func artifactStoreAddress(ctx context.Context, inv *inventory.Inventory,
|
||||
shelf map[string]catalogue.Manifest) (string, error) {
|
||||
shelf map[string]catalogue.Manifest, forNode string) (string, error) {
|
||||
onNetwork, err := whereEveryoneIs(ctx, inv, shelf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -508,8 +515,29 @@ func artifactStoreAddress(ctx context.Context, inv *inventory.Inventory,
|
||||
on[name] = true
|
||||
}
|
||||
node, port, found, err := artifactStoreOnNetwork(ctx, inv, on)
|
||||
if err != nil || !found {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if found {
|
||||
return overlay.InternalName(node) + ":" + port, nil
|
||||
}
|
||||
holder, port, found, err := artifactStoreHolder(ctx, inv)
|
||||
if err != nil || !found || holder != forNode {
|
||||
return "", err
|
||||
}
|
||||
return "127.0.0.1:" + port, nil
|
||||
}
|
||||
|
||||
// artifactStoreHolder is whichever node is assigned a module offering the artifact store, on or
|
||||
// off the network, and the port that node put it on.
|
||||
func artifactStoreHolder(ctx context.Context, inv *inventory.Inventory) (node, port string, found bool, err error) {
|
||||
nodes, err := inv.Nodes(ctx)
|
||||
if err != nil {
|
||||
return "", "", false, err
|
||||
}
|
||||
all := map[string]bool{}
|
||||
for _, n := range nodes {
|
||||
all[n.Name] = true
|
||||
}
|
||||
return artifactStoreOnNetwork(ctx, inv, all)
|
||||
}
|
||||
|
||||
+27
-27
@@ -481,17 +481,10 @@ func renderingFor(ctx context.Context, open *stores, node string,
|
||||
return catalogue.Rendering{}, inventory.Node{}, err
|
||||
}
|
||||
|
||||
// Where this node put the foundation's servers, for the control plane's own connections
|
||||
// (novox/hq 04-ISSUES/102): read from the node's settings for whatever claims each seat,
|
||||
// exactly as a consumer's binding is, never from what genesis wrote into a secret.
|
||||
seats, err := seatsOn(ctx, inv, shelf, node, plan.Modules)
|
||||
if err != nil {
|
||||
return catalogue.Rendering{}, inventory.Node{}, err
|
||||
}
|
||||
// And the artifact store as this network reaches it now — the address every image and
|
||||
// archive the mesh built is fetched through, composed here and recorded nowhere — with what
|
||||
// the mesh has built, so a reference recorded with an address before that is re-routed too.
|
||||
artifactStore, err := artifactStoreAddress(ctx, inv, shelf)
|
||||
// The artifact store as this node reaches it now — the address every image and archive the
|
||||
// mesh built is fetched through, composed here and recorded nowhere — with what the mesh has
|
||||
// built, so a reference recorded with an address before that is re-routed too.
|
||||
artifactStore, err := artifactStoreAddress(ctx, inv, shelf, node)
|
||||
if err != nil {
|
||||
return catalogue.Rendering{}, inventory.Node{}, err
|
||||
}
|
||||
@@ -571,6 +564,13 @@ func renderingFor(ctx context.Context, open *stores, node string,
|
||||
taken[m] = true
|
||||
}
|
||||
}
|
||||
// Where this node put the foundation's servers, for the control plane's own connections
|
||||
// (novox/hq 04-ISSUES/102): read from the node's settings for whatever claims each seat,
|
||||
// exactly as a consumer's binding is, never from what genesis wrote into a secret.
|
||||
seats, err := seatsOn(ctx, inv, shelf, node, plan.Modules, record.Adopted, taken)
|
||||
if err != nil {
|
||||
return catalogue.Rendering{}, inventory.Node{}, err
|
||||
}
|
||||
return catalogue.Rendering{
|
||||
Settings: settings, Generators: gens, Grants: grants, Needed: needed, Ports: ports,
|
||||
Certificate: certificate, Authority: authority, Mesh: private, Names: names,
|
||||
@@ -1023,7 +1023,7 @@ func portsGivenOn(ctx context.Context, inv *inventory.Inventory, node string,
|
||||
// and not yet assigned (04-ISSUES/085), and the control plane must follow that setting from the
|
||||
// first declaration it composes for itself. A holder in this node's set wins over one that is not.
|
||||
func seatsOn(ctx context.Context, inv *inventory.Inventory, shelf map[string]catalogue.Manifest,
|
||||
node string, inSet []catalogue.Manifest) (map[string]map[int]int, error) {
|
||||
node string, inSet []catalogue.Manifest, adopted bool, taken map[string]bool) (map[string]map[int]int, error) {
|
||||
assigned := map[string]bool{}
|
||||
for _, m := range inSet {
|
||||
assigned[m.Module] = true
|
||||
@@ -1045,26 +1045,26 @@ func seatsOn(ctx context.Context, inv *inventory.Inventory, shelf map[string]cat
|
||||
if len(claims) == 0 {
|
||||
continue
|
||||
}
|
||||
// **Only a port the node was given or the mesh assigned — never the manifest's own
|
||||
// number.** The sealed value the answer sits beside carries the port genesis wrote, which
|
||||
// on a given-port node is the predecessor's; a manifest's long-form mapping is the
|
||||
// catalogue's default, and answering with it would override the right number with one
|
||||
// the mesh never checked (the contract in seat_into.go). And on an adopted node a holder
|
||||
// assigned but not yet taken is the found container, on the ports it was found with, not
|
||||
// the declaration's — so its mesh-assigned ports do not count there either; a given port
|
||||
// does, because a given port is the found one by construction (ADR 0100).
|
||||
ports, _, err := portsGivenOn(ctx, inv, node, m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if assigned[name] {
|
||||
// Running here, so what its manifest publishes the long way is where this machine
|
||||
// has it — the same reading the declaration itself makes (ADR 0038). Only for a
|
||||
// holder in this node's set: a manifest's number says nothing about a machine the
|
||||
// module does not run on.
|
||||
var declared []int
|
||||
for _, l := range m.Listens {
|
||||
declared = append(declared, l.Port)
|
||||
}
|
||||
for _, wanted := range append(declared, m.Guards...) {
|
||||
if _, said := ports[wanted]; said {
|
||||
continue
|
||||
}
|
||||
if at, mayAssign := m.MachineSide(wanted); !mayAssign && at != 0 {
|
||||
ports[wanted] = at
|
||||
if adopted && !taken[name] {
|
||||
layers, err := inv.SettingsFor(ctx, node, m.Module)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ports = map[int]int{}
|
||||
if given, err := catalogue.GivenPorts(m, layers); err == nil {
|
||||
ports = given
|
||||
}
|
||||
}
|
||||
if len(ports) == 0 {
|
||||
|
||||
@@ -101,12 +101,50 @@ func routedArtifacts(made []inventory.Artifact, address string) []inventory.Arti
|
||||
return out
|
||||
}
|
||||
|
||||
// whereTheStoreIs is the artifact store's address as this network reaches it, or "" — read for a
|
||||
// caller that has the inventory open and nothing else in hand.
|
||||
func whereTheStoreIs(ctx context.Context, inv *inventory.Inventory) (string, error) {
|
||||
// whereTheStoreIs is the artifact store's address as something on `forNode` reaches it, or "" —
|
||||
// read for a caller that has the inventory open and nothing else in hand. With no node named, the
|
||||
// store's own node: loopback when nothing is on the network yet.
|
||||
func whereTheStoreIs(ctx context.Context, inv *inventory.Inventory, forNode string) (string, error) {
|
||||
shelf, err := inv.Catalogue(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return artifactStoreAddress(ctx, inv, shelf)
|
||||
if forNode == "" {
|
||||
if holder, _, found, err := artifactStoreHolder(ctx, inv); err != nil {
|
||||
return "", err
|
||||
} else if found {
|
||||
forNode = holder
|
||||
}
|
||||
}
|
||||
return artifactStoreAddress(ctx, inv, shelf, forNode)
|
||||
}
|
||||
|
||||
// whereABuilderReachesTheStore is the store's address for the machine that builds: the network's
|
||||
// when there is one, else loopback on the store's own node — when that node also holds a module
|
||||
// requiring the store, which is what a builder is (genesis: one node holds both).
|
||||
func whereABuilderReachesTheStore(ctx context.Context, inv *inventory.Inventory) (string, error) {
|
||||
shelf, err := inv.Catalogue(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
holder, _, found, err := artifactStoreHolder(ctx, inv)
|
||||
if err != nil || !found {
|
||||
return "", err
|
||||
}
|
||||
assigned, err := inv.Assigned(ctx, holder)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
besideIt := false
|
||||
for _, a := range assigned {
|
||||
for _, r := range shelf[a].Requires {
|
||||
if r == catalogue.ArtifactStoreProvision {
|
||||
besideIt = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !besideIt {
|
||||
holder = ""
|
||||
}
|
||||
return artifactStoreAddress(ctx, inv, shelf, holder)
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ func (f following) Announceable(ctx context.Context) ([]link.Announcement, error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
address, err := whereTheStoreIs(ctx, f.open.inventory)
|
||||
address, err := whereTheStoreIs(ctx, f.open.inventory, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -97,6 +97,15 @@ func Rerouted(reference, address string) string {
|
||||
//
|
||||
// A kept reference with no store to route it through is refused: sent as it is, the runtime would
|
||||
// refuse the scheme on the machine, one push away from the reason.
|
||||
//
|
||||
// **What this does not reach: the images genesis pinned.** The installer builds the control plane
|
||||
// and the builder before the mesh exists, pushes them itself and pins their manifests to
|
||||
// `<registry>:<port>/mesh-controller@…` and `<registry>:<port>/mesh-builder@…` — single-segment
|
||||
// repositories with no build record, so `with.Built` does not name them and they are left as
|
||||
// written until each is rebuilt through the mesh, which records it by digest and path. Until then
|
||||
// a registry that moves strands exactly those two on a recreate, and the control plane's is the
|
||||
// one that cannot be repaired through the mesh. Rebuild both through `build` before moving the
|
||||
// store (novox/hq 04-ISSUES/102, finding F4).
|
||||
func artifactsInto(resource map[string]any, module string, with Rendering) error {
|
||||
for _, key := range []string{"image", "source"} {
|
||||
written, ok := resource[key].(string)
|
||||
|
||||
@@ -101,6 +101,19 @@ func TestTheControlPlanesOwnAddressesFollowTheNodesPorts(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("the control plane's own manifest does not parse:\n%v", err)
|
||||
}
|
||||
// The manifest itself names them now; withSeatPorts is a no-op on it, and this holds it so.
|
||||
for _, r := range m.Resources {
|
||||
if r["type"] != "container" {
|
||||
continue
|
||||
}
|
||||
env, _ := r["env"].(map[string]any)
|
||||
for key, want := range SeatPorts {
|
||||
if env[key] != want {
|
||||
t.Errorf("module.json says %s=%v, not %q", key, env[key], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
m = withSeatPorts(m)
|
||||
control, err := m.Resolve([]Built{{
|
||||
Name: "server", Kind: ArtifactImage,
|
||||
Reference: ArtifactStoreScheme + "mesh-controller/server@sha256:" + strings.Repeat("c", 64),
|
||||
@@ -155,3 +168,49 @@ func TestTheControlPlanesOwnAddressesFollowTheNodesPorts(t *testing.T) {
|
||||
t.Errorf("with no settings, the control plane is told %v", env)
|
||||
}
|
||||
}
|
||||
|
||||
// SeatPorts is what the control plane's manifest says beside each sealed connection: the port
|
||||
// this machine put the seat's holder at (novox/hq 04-ISSUES/102).
|
||||
var SeatPorts = map[string]string{
|
||||
"MESH_STORE_INVENTORY_PORT": "${seat:mesh-store:5432}",
|
||||
"MESH_STORE_IDENTITY_PORT": "${seat:mesh-store:5432}",
|
||||
"MESH_STORE_LICENCES_PORT": "${seat:mesh-store:5432}",
|
||||
"MESH_BROKER_AMQP_PORT": "${seat:mesh-broker:5672}",
|
||||
"MESH_BROKER_MANAGEMENT_PORT": "${seat:mesh-broker:15672}",
|
||||
"MESH_BROKER_ADDRESS_PORT": "${seat:mesh-broker:5671}",
|
||||
}
|
||||
|
||||
// withSeatPorts is the control plane's manifest with SeatPorts in its container's environment.
|
||||
//
|
||||
// **The manifest lands one commit after the code that fills it**, deliberately: a control plane
|
||||
// still running the previous build passes `${seat:…}` through unfilled, and the manifest may only
|
||||
// name the placeholder once every control plane that could compose it knows it. So the test does
|
||||
// not depend on module.json carrying these yet, and is a no-op once it does.
|
||||
func withSeatPorts(m Manifest) Manifest {
|
||||
out := m
|
||||
out.Resources = nil
|
||||
for _, r := range m.Resources {
|
||||
if r["type"] != "container" {
|
||||
out.Resources = append(out.Resources, r)
|
||||
continue
|
||||
}
|
||||
copied := map[string]any{}
|
||||
for k, v := range r {
|
||||
copied[k] = v
|
||||
}
|
||||
env := map[string]any{}
|
||||
if had, ok := r["env"].(map[string]any); ok {
|
||||
for k, v := range had {
|
||||
env[k] = v
|
||||
}
|
||||
}
|
||||
for k, v := range SeatPorts {
|
||||
if _, said := env[k]; !said {
|
||||
env[k] = v
|
||||
}
|
||||
}
|
||||
copied["env"] = env
|
||||
out.Resources = append(out.Resources, copied)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -40,6 +40,20 @@ func Port(name string) (string, error) {
|
||||
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)
|
||||
@@ -70,6 +84,10 @@ func Placed(name string) (string, error) {
|
||||
// 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
|
||||
@@ -84,14 +102,19 @@ func WithPort(value, port string) (string, error) {
|
||||
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:]
|
||||
// **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:]
|
||||
}
|
||||
userinfo := ""
|
||||
if at := strings.LastIndex(authority, "@"); at >= 0 {
|
||||
userinfo, authority = authority[:at+1], authority[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 {
|
||||
|
||||
@@ -16,6 +16,7 @@ func TestAPortTwinMovesTheValuesPort(t *testing.T) {
|
||||
"amqp://control:p%40ss@127.0.0.1:5672/": "amqp://control:p%40ss@127.0.0.1:6852/",
|
||||
"amqp://control:p@ss:with@127.0.0.1:5672/": "amqp://control:p@ss:with@127.0.0.1:6852/",
|
||||
"http://guest:guest@127.0.0.1:15672": "http://guest:guest@127.0.0.1:6852",
|
||||
"postgres://mesh:a/b?c#d@e@127.0.0.1:5432/inventory": "postgres://mesh:a/b?c#d@e@127.0.0.1:6852/inventory",
|
||||
"http://[::1]:15672/api": "http://[::1]:6852/api",
|
||||
"broker.example:5671": "broker.example:6852",
|
||||
"broker.example": "broker.example:6852",
|
||||
@@ -64,3 +65,14 @@ func TestAPortTwinThatIsNotAPortIsRefusedWithoutQuotingTheValue(t *testing.T) {
|
||||
t.Fatal("a port with no value to put it on was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
// A placeholder nothing filled is nothing said, not a fault: the control plane composing the
|
||||
// declaration may be one build behind the manifest, and refusing would leave it headless.
|
||||
func TestAnUnfilledPlaceholderIsNothingSaid(t *testing.T) {
|
||||
t.Setenv("MESH_R", "postgres://m:p@127.0.0.1:5432/inventory")
|
||||
t.Setenv("MESH_R_PORT", "${seat:mesh-store:5432}")
|
||||
got, err := Placed("MESH_R")
|
||||
if err != nil || got != "postgres://m:p@127.0.0.1:5432/inventory" {
|
||||
t.Fatalf("an unfilled placeholder was not ignored: %q, %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// **The manifest's placeholder, unfilled, reaches a store reader and changes nothing.**
|
||||
//
|
||||
// The control plane composes its own declaration, so the manifest naming `${seat:…}` can be
|
||||
// composed by a control plane one build older than it — one that passes the literal through as
|
||||
// the value. That is the state of the live control-node between this manifest landing and its
|
||||
// next build being pushed, and a reader that refused the literal would leave it headless
|
||||
// (novox/hq 04-ISSUES/102, finding F1). So the reader is held to ignoring exactly what
|
||||
// module.json says, not a placeholder shaped like it.
|
||||
func TestTheManifestsOwnPlaceholderUnfilledLeavesTheStoreWhereTheFileSays(t *testing.T) {
|
||||
raw, err := os.ReadFile("../../module.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m struct {
|
||||
Resources []struct {
|
||||
Type string `json:"type"`
|
||||
Env map[string]string `json:"env"`
|
||||
} `json:"resources"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var written string
|
||||
for _, r := range m.Resources {
|
||||
if r.Type == "container" {
|
||||
written = r.Env["MESH_STORE_INVENTORY_PORT"]
|
||||
}
|
||||
}
|
||||
if written == "" {
|
||||
t.Fatal("module.json no longer names MESH_STORE_INVENTORY_PORT")
|
||||
}
|
||||
|
||||
alone(t)
|
||||
t.Setenv(Variable(example), dsn)
|
||||
t.Setenv(PortVariable(example), written)
|
||||
opened, err := Open(t.Context(), example)
|
||||
if err != nil {
|
||||
t.Fatalf("the unfilled placeholder was refused, which is a headless control plane: %v", err)
|
||||
}
|
||||
defer opened.Close()
|
||||
if got := opened.Pool().Config().ConnConfig.Port; got != 5432 {
|
||||
t.Fatalf("the store is on %d; with the placeholder unfilled, the file's port stands", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user