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
158 lines
5.8 KiB
Go
158 lines
5.8 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The control plane's own addresses follow the node's ports (novox/hq 04-ISSUES/102).
|
|
|
|
func TestASeatPlaceholderAnswersWhereThisMachinePutTheHolder(t *testing.T) {
|
|
control := map[string]any{
|
|
"type": "container", "id": "server", "name": "mesh-controller",
|
|
"env": map[string]any{
|
|
"MESH_STORE_INVENTORY_PORT": "${seat:mesh-store:5432}",
|
|
"MESH_BROKER_AMQP_PORT": "${seat:mesh-broker:5672}",
|
|
"MESH_BROKER_ADDRESS_PORT": "${seat:mesh-broker:5671}",
|
|
"MESH_STORE_INVENTORY_FILE": "/run/secrets/inventory",
|
|
},
|
|
}
|
|
with := Rendering{Seats: map[string]map[int]int{
|
|
"mesh-store": {5432: 6852}, "mesh-broker": {5672: 5679, 5671: 5671},
|
|
}}
|
|
if err := seatInto(control, "mesh-controller", with); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
env := control["env"].(map[string]any)
|
|
for key, want := range map[string]string{
|
|
"MESH_STORE_INVENTORY_PORT": "6852",
|
|
"MESH_BROKER_AMQP_PORT": "5679",
|
|
"MESH_BROKER_ADDRESS_PORT": "5671",
|
|
"MESH_STORE_INVENTORY_FILE": "/run/secrets/inventory",
|
|
} {
|
|
if env[key] != want {
|
|
t.Errorf("%s = %v, want %q", key, env[key], want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A seat nothing on this machine holds — or one whose holder the mesh has given no port — answers
|
|
// with nothing, so the port genesis wrote into the connection string stands. Not the software's
|
|
// own port: on a node given a port at genesis before the store's module exists, that would
|
|
// override the right number with the catalogue's.
|
|
func TestASeatTheMeshCannotPlaceAnswersWithNothing(t *testing.T) {
|
|
control := map[string]any{
|
|
"type": "container", "id": "server", "name": "mesh-controller",
|
|
"env": map[string]any{"MESH_STORE_INVENTORY_PORT": "${seat:mesh-store:5432}"},
|
|
}
|
|
if err := seatInto(control, "mesh-controller", Rendering{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := control["env"].(map[string]any)["MESH_STORE_INVENTORY_PORT"]; got != "" {
|
|
t.Fatalf("with nothing known, the seat answered %q", got)
|
|
}
|
|
file := map[string]any{"type": "file", "content": "port=${seat:mesh-store:5432}\n"}
|
|
if err := seatInto(file, "x", Rendering{Seats: map[string]map[int]int{"mesh-store": {5433: 1}}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := file["content"]; got != "port=\n" {
|
|
t.Fatalf("a port the holder does not publish answered %q", got)
|
|
}
|
|
}
|
|
|
|
func TestASeatPlaceholderNamingNoPortIsRefused(t *testing.T) {
|
|
file := map[string]any{"type": "file", "content": "${seat:mesh-store:99999}"}
|
|
if err := seatInto(file, "x", Rendering{}); err == nil {
|
|
t.Fatal("99999 was accepted as a port")
|
|
}
|
|
}
|
|
|
|
func TestFillingASeatLeavesTheManifestAlone(t *testing.T) {
|
|
env := map[string]any{"MESH_STORE_INVENTORY_PORT": "${seat:mesh-store:5432}"}
|
|
manifest := map[string]any{"type": "container", "id": "server", "env": env}
|
|
for _, at := range []int{6852, 5432} {
|
|
copied := map[string]any{}
|
|
for k, v := range manifest {
|
|
copied[k] = v
|
|
}
|
|
with := Rendering{Seats: map[string]map[int]int{"mesh-store": {5432: at}}}
|
|
if err := seatInto(copied, "mesh-controller", with); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if env["MESH_STORE_INVENTORY_PORT"] != "${seat:mesh-store:5432}" {
|
|
t.Fatalf("the module's own manifest was edited: %v", env)
|
|
}
|
|
}
|
|
|
|
// **The control plane's own manifest, composed through the whole path.**
|
|
//
|
|
// The store was given 6852 and the broker's plain port 5679 (the control-node's migration, novox/hq
|
|
// 04-ISSUES/102). The control plane's own connections are sealed at genesis with the ports genesis
|
|
// wrote; what its container is told beside them is where this machine put the store and the
|
|
// broker now, read from the node's settings exactly as every consumer's binding is.
|
|
func TestTheControlPlanesOwnAddressesFollowTheNodesPorts(t *testing.T) {
|
|
raw, err := os.ReadFile("../../module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m, err := ParseManifest(raw)
|
|
if err != nil {
|
|
t.Fatalf("the control plane's own manifest does not parse:\n%v", err)
|
|
}
|
|
control, err := m.Resolve([]Built{{
|
|
Name: "server", Kind: ArtifactImage,
|
|
Reference: ArtifactStoreScheme + "mesh-controller/server@sha256:" + strings.Repeat("c", 64),
|
|
}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := Resolution{Node: "anchor", Modules: []Manifest{control}}
|
|
needed := map[string]map[string]string{"mesh-controller": {}}
|
|
for name := range m.OwnSecrets {
|
|
needed["mesh-controller"][name] = "sealed-" + name
|
|
}
|
|
out, err := r.Declaration(Rendering{
|
|
Needed: needed,
|
|
ArtifactStore: "anchor.internal:5100",
|
|
Seats: map[string]map[int]int{
|
|
"mesh-store": {5432: 6852},
|
|
"mesh-broker": {5671: 5671, 5672: 5679, 15672: 15673},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("the control plane does not compose: %v", err)
|
|
}
|
|
server := fileNamed(out, "mesh-controller.server")
|
|
if server == nil {
|
|
t.Fatalf("the control plane's container is not in the declaration: %v", out)
|
|
}
|
|
env, _ := server["env"].(map[string]any)
|
|
for key, want := range map[string]string{
|
|
"MESH_STORE_INVENTORY_PORT": "6852",
|
|
"MESH_STORE_IDENTITY_PORT": "6852",
|
|
"MESH_STORE_LICENCES_PORT": "6852",
|
|
"MESH_BROKER_AMQP_PORT": "5679",
|
|
"MESH_BROKER_MANAGEMENT_PORT": "15673",
|
|
"MESH_BROKER_ADDRESS_PORT": "5671",
|
|
} {
|
|
if env[key] != want {
|
|
t.Errorf("the control plane is told %s=%v; the node put it on %s", key, env[key], want)
|
|
}
|
|
}
|
|
if got := server["image"]; got != "anchor.internal:5100/mesh-controller/server@sha256:"+strings.Repeat("c", 64) {
|
|
t.Errorf("the control plane's own image is %v, not routed through the store", got)
|
|
}
|
|
|
|
// And on a mesh where the foundation is where genesis raised it, nothing is added.
|
|
out, err = r.Declaration(Rendering{Needed: needed, ArtifactStore: "anchor.internal:5100"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
env, _ = fileNamed(out, "mesh-controller.server")["env"].(map[string]any)
|
|
if env["MESH_STORE_INVENTORY_PORT"] != "" || env["MESH_BROKER_AMQP_PORT"] != "" {
|
|
t.Errorf("with no settings, the control plane is told %v", env)
|
|
}
|
|
}
|