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/catalogue"
|
||||||
"github.com/novox/mesh-controller/internal/inventory"
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
|
"github.com/novox/mesh-controller/internal/licences"
|
||||||
"github.com/novox/mesh-controller/internal/link"
|
"github.com/novox/mesh-controller/internal/link"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -153,7 +154,7 @@ func TestTheControlPlaneIsToldWhereTheNodePutTheStoreAndTheBroker(t *testing.T)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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}})
|
Reference: "registry.example/control@" + aDigest}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -198,15 +199,137 @@ func TestTheControlPlaneIsToldWhereTheNodePutTheStoreAndTheBroker(t *testing.T)
|
|||||||
t.Fatal("the control plane's container is not in its own node's declaration")
|
t.Fatal("the control plane's container is not in its own node's declaration")
|
||||||
}
|
}
|
||||||
for key, want := range map[string]string{
|
for key, want := range map[string]string{
|
||||||
"MESH_STORE_INVENTORY_PORT": "6852",
|
"MESH_STORE_INVENTORY_PORT": "6852",
|
||||||
"MESH_STORE_IDENTITY_PORT": "6852",
|
"MESH_STORE_IDENTITY_PORT": "6852",
|
||||||
"MESH_STORE_LICENCES_PORT": "6852",
|
"MESH_STORE_LICENCES_PORT": "6852",
|
||||||
"MESH_BROKER_AMQP_PORT": "5679",
|
"MESH_BROKER_AMQP_PORT": "5679",
|
||||||
"MESH_BROKER_ADDRESS_PORT": "5671",
|
// Neither given nor assigned by the mesh: the manifest's own number is NOT the answer,
|
||||||
"MESH_BROKER_MANAGEMENT_PORT": "15672",
|
// 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 {
|
if env[key] != want {
|
||||||
t.Errorf("the control plane is told %s=%v; the node says %q", key, 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)
|
fmt.Fprintf(os.Stderr, "could not read what this mesh has built: %v\n", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
address, err := whereTheStoreIs(ctx, open.inventory)
|
address, err := whereABuilderReachesTheStore(ctx, open.inventory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "could not find the artifact store on this mesh's network, so a "+
|
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)
|
"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
|
return "", "", false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// artifactStoreAddress is the artifact store as this network reaches it, `<node>.internal:<port>`,
|
// artifactStoreAddress is the artifact store as `forNode` reaches it: `<node>.internal:<port>`
|
||||||
// or "" when the mesh has none on its network yet — the address composed into every reference
|
// over the private network, or — when nothing is on the network yet — `127.0.0.1:<port>` for the
|
||||||
// the mesh built, at the moment it is used and never before (novox/hq 04-ISSUES/102).
|
// 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,
|
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)
|
onNetwork, err := whereEveryoneIs(ctx, inv, shelf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -508,8 +515,29 @@ func artifactStoreAddress(ctx context.Context, inv *inventory.Inventory,
|
|||||||
on[name] = true
|
on[name] = true
|
||||||
}
|
}
|
||||||
node, port, found, err := artifactStoreOnNetwork(ctx, inv, on)
|
node, port, found, err := artifactStoreOnNetwork(ctx, inv, on)
|
||||||
if err != nil || !found {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return overlay.InternalName(node) + ":" + port, nil
|
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
|
return catalogue.Rendering{}, inventory.Node{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Where this node put the foundation's servers, for the control plane's own connections
|
// The artifact store as this node reaches it now — the address every image and archive the
|
||||||
// (novox/hq 04-ISSUES/102): read from the node's settings for whatever claims each seat,
|
// mesh built is fetched through, composed here and recorded nowhere — with what the mesh has
|
||||||
// exactly as a consumer's binding is, never from what genesis wrote into a secret.
|
// built, so a reference recorded with an address before that is re-routed too.
|
||||||
seats, err := seatsOn(ctx, inv, shelf, node, plan.Modules)
|
artifactStore, err := artifactStoreAddress(ctx, inv, shelf, node)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return catalogue.Rendering{}, inventory.Node{}, err
|
return catalogue.Rendering{}, inventory.Node{}, err
|
||||||
}
|
}
|
||||||
@@ -571,6 +564,13 @@ func renderingFor(ctx context.Context, open *stores, node string,
|
|||||||
taken[m] = true
|
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{
|
return catalogue.Rendering{
|
||||||
Settings: settings, Generators: gens, Grants: grants, Needed: needed, Ports: ports,
|
Settings: settings, Generators: gens, Grants: grants, Needed: needed, Ports: ports,
|
||||||
Certificate: certificate, Authority: authority, Mesh: private, Names: names,
|
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
|
// 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.
|
// 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,
|
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{}
|
assigned := map[string]bool{}
|
||||||
for _, m := range inSet {
|
for _, m := range inSet {
|
||||||
assigned[m.Module] = true
|
assigned[m.Module] = true
|
||||||
@@ -1045,26 +1045,26 @@ func seatsOn(ctx context.Context, inv *inventory.Inventory, shelf map[string]cat
|
|||||||
if len(claims) == 0 {
|
if len(claims) == 0 {
|
||||||
continue
|
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)
|
ports, _, err := portsGivenOn(ctx, inv, node, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if assigned[name] {
|
if adopted && !taken[name] {
|
||||||
// Running here, so what its manifest publishes the long way is where this machine
|
layers, err := inv.SettingsFor(ctx, node, m.Module)
|
||||||
// has it — the same reading the declaration itself makes (ADR 0038). Only for a
|
if err != nil {
|
||||||
// holder in this node's set: a manifest's number says nothing about a machine the
|
return nil, err
|
||||||
// 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...) {
|
ports = map[int]int{}
|
||||||
if _, said := ports[wanted]; said {
|
if given, err := catalogue.GivenPorts(m, layers); err == nil {
|
||||||
continue
|
ports = given
|
||||||
}
|
|
||||||
if at, mayAssign := m.MachineSide(wanted); !mayAssign && at != 0 {
|
|
||||||
ports[wanted] = at
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(ports) == 0 {
|
if len(ports) == 0 {
|
||||||
|
|||||||
@@ -101,12 +101,50 @@ func routedArtifacts(made []inventory.Artifact, address string) []inventory.Arti
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// whereTheStoreIs is the artifact store's address as this network reaches it, or "" — read for a
|
// whereTheStoreIs is the artifact store's address as something on `forNode` reaches it, or "" —
|
||||||
// caller that has the inventory open and nothing else in hand.
|
// read for a caller that has the inventory open and nothing else in hand. With no node named, the
|
||||||
func whereTheStoreIs(ctx context.Context, inv *inventory.Inventory) (string, error) {
|
// 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)
|
shelf, err := inv.Catalogue(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
address, err := whereTheStoreIs(ctx, f.open.inventory)
|
address, err := whereTheStoreIs(ctx, f.open.inventory, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// 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.
|
// 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 {
|
func artifactsInto(resource map[string]any, module string, with Rendering) error {
|
||||||
for _, key := range []string{"image", "source"} {
|
for _, key := range []string{"image", "source"} {
|
||||||
written, ok := resource[key].(string)
|
written, ok := resource[key].(string)
|
||||||
|
|||||||
@@ -101,6 +101,19 @@ func TestTheControlPlanesOwnAddressesFollowTheNodesPorts(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("the control plane's own manifest does not parse:\n%v", err)
|
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{{
|
control, err := m.Resolve([]Built{{
|
||||||
Name: "server", Kind: ArtifactImage,
|
Name: "server", Kind: ArtifactImage,
|
||||||
Reference: ArtifactStoreScheme + "mesh-controller/server@sha256:" + strings.Repeat("c", 64),
|
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)
|
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 == "" {
|
if raw == "" {
|
||||||
return "", nil
|
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)
|
n, err := strconv.Atoi(raw)
|
||||||
if err != nil || n < 1 || n > 65535 {
|
if err != nil || n < 1 || n > 65535 {
|
||||||
return "", fmt.Errorf("%s is %q, which is not a port", PortVar(name), raw)
|
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.
|
// keywordPort is `port=…` in a `key=value` connection string.
|
||||||
var keywordPort = regexp.MustCompile(`(^|\s)port=\S*`)
|
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`.
|
// WithPort is the value with its port replaced by `port`.
|
||||||
//
|
//
|
||||||
// Three shapes, because the control plane's settings come in three: a URL
|
// 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")
|
return "", fmt.Errorf("the value is empty")
|
||||||
}
|
}
|
||||||
if scheme, rest, isURL := strings.Cut(value, "://"); isURL {
|
if scheme, rest, isURL := strings.Cut(value, "://"); isURL {
|
||||||
end := strings.IndexAny(rest, "/?#")
|
// **The password may hold any of `/ ? # @`, so the host begins after the LAST `@`**, and
|
||||||
authority, tail := rest, ""
|
// the path only after that. Cutting at the first `/` would take a password's slash for the
|
||||||
if end >= 0 {
|
// path's and put the new port on the user name — a connection string that dials the wrong
|
||||||
authority, tail = rest[:end], rest[end:]
|
// 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 := ""
|
authority, tail := hostAndTail, ""
|
||||||
if at := strings.LastIndex(authority, "@"); at >= 0 {
|
if end := strings.IndexAny(hostAndTail, "/?#"); end >= 0 {
|
||||||
userinfo, authority = authority[:at+1], authority[at+1:]
|
authority, tail = hostAndTail[:end], hostAndTail[end:]
|
||||||
}
|
}
|
||||||
host, err := hostWithPort(authority, port)
|
host, err := hostWithPort(authority, port)
|
||||||
if err != nil {
|
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%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/",
|
"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",
|
"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",
|
"http://[::1]:15672/api": "http://[::1]:6852/api",
|
||||||
"broker.example:5671": "broker.example:6852",
|
"broker.example:5671": "broker.example:6852",
|
||||||
"broker.example": "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")
|
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