The controller is a Go program and was the one piece of the mesh's own Go code still shipped and run as an image (novox/hq issue 213; ADR 0188 §1: a module's own code is bundles; §3: a service bundle is a process). The manifest now builds one Go bundle, `controller`, and runs it as the process `mesh-controller` (`./mesh-controller serve`) under an account the module declares. What the container gave it, replaced: - host network: a process is on the host's network; nothing it reads names a container network - user 65534: the account `mesh-controller`, which owns its secrets and its state directory - the eight mounts: the env names the host paths the mesh already places (the store, broker and bus files under the state directory, the broker's certificate under /var/lib/mesh-broker-tls); the `broker` mount was read by nothing and is gone with the others - `container-runtime` is no longer required on its machine Its preparation is the same binary with `prepare`, as a run-once process, and the process `replaces` the container `server`: the host keeps the container answering until the process is running (mesh-host). Needs the previous commit live in the running controller, and the host's `replaces` on the controller's machine, before it is registered. No image is built by the mesh any more. The Dockerfile stays for genesis and the lab (`make image`, its Go base now pinned in the Makefile).
214 lines
7.6 KiB
Go
214 lines
7.6 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_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_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)
|
|
}
|
|
// 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"] != "process" {
|
|
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: "controller", Kind: ArtifactBundle,
|
|
Reference: ArtifactStoreScheme + "mesh-controller/controller@sha256:" + strings.Repeat("c", 64),
|
|
Digest: "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.controller")
|
|
if server == nil {
|
|
t.Fatalf("the control plane's process 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_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["source"]; got != "anchor.internal:5100/mesh-controller/controller@sha256:"+strings.Repeat("c", 64) {
|
|
t.Errorf("the control plane's own bundle is fetched from %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.controller")["env"].(map[string]any)
|
|
if env["MESH_STORE_INVENTORY_PORT"] != "" {
|
|
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_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" && r["type"] != "process" {
|
|
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
|
|
}
|