ADR 0593 §7: the composer with its refusals leaves mesh-sdk, so mesh-sdk carries no composition rule. The catalogue functions only the node-engine's composition reaches (Resolve, Resolution's Compose, Declaration, DataItems, LeftOut, Rules and the rest, the settings, placeholder and template refusals, the rendering helpers) are copied with their tests into internal/compose/resolution; a method of a mesh-sdk shape becomes a function taking it first. The composition calls its own copies, and the captured catalogue its tests read lives here now, shared by mesh-compose's tests. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
173 lines
7.6 KiB
Go
173 lines
7.6 KiB
Go
package resolution
|
|
|
|
import (
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// **A process binds the port the mesh gave it, not the one it wrote down.**
|
|
//
|
|
// A container never needed this: the mesh rewrites its `ports` into assigned:wanted, so the
|
|
// software binds the number it always bound and the machine publishes another. A process runs on
|
|
// the machine with nothing to rewrite, so without a way to ask, every process binds the number in
|
|
// its own configuration and two modules declaring the same one collide — which is the whole
|
|
// problem ADR 0038 exists to prevent, reintroduced by the resource kind that most needs it.
|
|
func TestAModuleIsToldWhichPortItWasGiven(t *testing.T) {
|
|
file := map[string]any{
|
|
"type": "file", "id": "settings",
|
|
"content": "LISTEN=${port:8080}\n",
|
|
}
|
|
listens := []mcat.Listening{{Port: 8080, From: mcat.FromMesh}}
|
|
with := mcat.Rendering{Ports: map[string]map[int]int{"showcase": {8080: 21000}}}
|
|
|
|
if err := portInto(file, "showcase", listens, with); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := file["content"].(string); got != "LISTEN=21000\n" {
|
|
t.Fatalf("the module was not told its assigned port: %q", got)
|
|
}
|
|
}
|
|
|
|
// With nothing assigned yet, it is told the port it asked about — so a mesh that has not made an
|
|
// assignment still composes something coherent rather than writing a zero.
|
|
func TestWithNoAssignmentAModuleIsToldWhatItAskedFor(t *testing.T) {
|
|
file := map[string]any{"type": "file", "content": "LISTEN=${port:8080}\n"}
|
|
if err := portInto(file, "showcase", []mcat.Listening{{Port: 8080}}, mcat.Rendering{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := file["content"].(string); got != "LISTEN=8080\n" {
|
|
t.Fatalf("an unassigned port did not fall back to what was declared: %q", got)
|
|
}
|
|
}
|
|
|
|
// **Asking about a port it never declared is refused**, and the refusal says what it did declare.
|
|
// The module is asking about something the mesh has no opinion on, and answering would be a guess
|
|
// written into a configuration file as a port number.
|
|
func TestAskingAboutAnUndeclaredPortIsRefused(t *testing.T) {
|
|
file := map[string]any{"type": "file", "content": "LISTEN=${port:9999}\n"}
|
|
err := portInto(file, "showcase", []mcat.Listening{{Port: 8080}}, mcat.Rendering{})
|
|
if err == nil {
|
|
t.Fatal("a module was told a port it never said it listens on")
|
|
}
|
|
if !strings.Contains(err.Error(), "8080") {
|
|
t.Fatalf("the refusal does not say what would have worked: %v", err)
|
|
}
|
|
}
|
|
|
|
// And a file mentioning no port is left exactly as it was.
|
|
func TestAFileWithNoPortIsUntouched(t *testing.T) {
|
|
file := map[string]any{"type": "file", "content": "GREETING=hello\n"}
|
|
if err := portInto(file, "showcase", nil, mcat.Rendering{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := file["content"].(string); got != "GREETING=hello\n" {
|
|
t.Fatalf("a file with no port was changed: %q", got)
|
|
}
|
|
}
|
|
|
|
// **A container that dials the machine is told the same thing, in its environment.**
|
|
//
|
|
// The forge's own sidecar reaches the forge over the machine's loopback (novox/hq 04-ISSUES/088).
|
|
// The `assigned:wanted` mapping that lets the forge itself go on binding 3000 does nothing for the
|
|
// caller: the caller dials the machine, so it must be given the machine's number — here one the
|
|
// node was given rather than one the mesh assigned, which is the case that makes the literal wrong
|
|
// even on a mesh that never allocates (ADR 0100).
|
|
func TestAContainerIsToldWhichPortItWasGiven(t *testing.T) {
|
|
sidecar := map[string]any{
|
|
"type": "container", "id": "runtime", "name": "mesh-gitea",
|
|
"env": map[string]any{
|
|
"MESH_GITEA_URL": "http://127.0.0.1:${port:3000}",
|
|
"MESH_ADMIN_USER": "mesh-admin",
|
|
},
|
|
}
|
|
listens := []mcat.Listening{{Port: 3000, From: mcat.FromMesh}}
|
|
with := mcat.Rendering{Given: map[string]map[int]int{"gitea": {3000: 2999}}}
|
|
|
|
if err := portInto(sidecar, "gitea", listens, with); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
env := sidecar["env"].(map[string]any)
|
|
if env["MESH_GITEA_URL"] != "http://127.0.0.1:2999" {
|
|
t.Fatalf("the sidecar dials %v, not the port this machine puts the forge on",
|
|
env["MESH_GITEA_URL"])
|
|
}
|
|
if env["MESH_ADMIN_USER"] != "mesh-admin" {
|
|
t.Fatalf("filling one value changed another: %v", env)
|
|
}
|
|
}
|
|
|
|
// **And the manifest it came out of is left alone.**
|
|
//
|
|
// An `env` map is the catalogue's, shared by every node running the module, and the resource
|
|
// around it is a shallow copy. Filled in place, the first node composed would write its own port
|
|
// into the catalogue and every node composed after it would be told that one — a fault that is
|
|
// invisible in a single composition and wrong in every mesh with two machines.
|
|
func TestFillingAContainersEnvironmentLeavesTheManifestAlone(t *testing.T) {
|
|
env := map[string]any{"MESH_GITEA_URL": "http://127.0.0.1:${port:3000}"}
|
|
manifest := map[string]any{"type": "container", "id": "runtime", "name": "mesh-gitea", "env": env}
|
|
|
|
for _, at := range []int{2999, 3100} {
|
|
copied := map[string]any{}
|
|
for k, v := range manifest {
|
|
copied[k] = v
|
|
}
|
|
with := mcat.Rendering{Given: map[string]map[int]int{"gitea": {3000: at}}}
|
|
if err := portInto(copied, "gitea", []mcat.Listening{{Port: 3000}}, with); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := "http://127.0.0.1:" + strconv.Itoa(at)
|
|
if got := copied["env"].(map[string]any)["MESH_GITEA_URL"]; got != want {
|
|
t.Fatalf("the second node was told %v, not %v — the first composition wrote its own "+
|
|
"port into the catalogue", got, want)
|
|
}
|
|
}
|
|
if env["MESH_GITEA_URL"] != "http://127.0.0.1:${port:3000}" {
|
|
t.Fatalf("the module's own manifest was edited: %v", env)
|
|
}
|
|
}
|
|
|
|
// Asking in an environment about a port it never declared is refused exactly as a file's is, and
|
|
// the refusal names the container and the variable — there is no line number to go on.
|
|
func TestAContainerAskingAboutAnUndeclaredPortIsRefused(t *testing.T) {
|
|
sidecar := map[string]any{
|
|
"type": "container", "id": "runtime", "name": "mesh-gitea",
|
|
"env": map[string]any{"MESH_GITEA_URL": "http://127.0.0.1:${port:9999}"},
|
|
}
|
|
err := portInto(sidecar, "gitea", []mcat.Listening{{Port: 3000}}, mcat.Rendering{})
|
|
if err == nil {
|
|
t.Fatal("a container was told a port its module never said it listens on")
|
|
}
|
|
for _, said := range []string{"mesh-gitea", "MESH_GITEA_URL", "3000"} {
|
|
if !strings.Contains(err.Error(), said) {
|
|
t.Errorf("the refusal does not say %q: %v", said, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// And a container naming no port keeps the environment it was written with — the same map, so
|
|
// nothing is copied and no other node's composition is disturbed.
|
|
func TestAContainerWithNoPortPlaceholderKeepsItsEnvironment(t *testing.T) {
|
|
env := map[string]any{"MESH_GITEA_URL": "http://gitea:3000", "PORT": 3000}
|
|
sidecar := map[string]any{"type": "container", "name": "mesh-gitea", "env": env}
|
|
if err := portInto(sidecar, "gitea", []mcat.Listening{{Port: 3000}}, mcat.Rendering{
|
|
Given: map[string]map[int]int{"gitea": {3000: 2999}},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A sibling reached over the runtime's own network is reached on the port INSIDE it, which the
|
|
// module does control: a literal there is right, and moving it would break the one address the
|
|
// mapping does not touch.
|
|
if got := sidecar["env"].(map[string]any)["MESH_GITEA_URL"]; got != "http://gitea:3000" {
|
|
t.Fatalf("an address on the runtime's own network was moved to the machine's port: %v", got)
|
|
}
|
|
// The same map, not a copy of it: a container that asks for nothing is left alone, and
|
|
// comparing the contents would say yes even to a rebuilt map.
|
|
if reflect.ValueOf(sidecar["env"]).Pointer() != reflect.ValueOf(env).Pointer() {
|
|
t.Fatalf("a container that asked for nothing had its environment rebuilt: %v", sidecar["env"])
|
|
}
|
|
}
|