Files
mesh-host/internal/compose/resolution/bound_into_files_test.go
T
jschoubben d68e241724 Take the catalogue's composition rules into the node-engine
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
2026-11-02 19:15:55 +01:00

247 lines
9.9 KiB
Go

package resolution
import (
"fmt"
"strings"
"testing"
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
)
// A consumer writes its own connection string (novox/hq 04-ISSUES/023).
//
// Everything needed for one now reaches the file that needs it: the address and port from what the
// provider serves, the name to present from what the mesh decided, the database name from what the
// module itself contributed, and the password through the sealed hole the host fills.
func TestAConsumerCanWriteAConnectionString(t *testing.T) {
r := mcat.Resolution{
Node: "workstation",
Modules: []mcat.Manifest{{
Module: "keycloak",
Requires: []string{"postgres-database"},
Secrets: map[string]string{"postgres-database": "/var/lib/keycloak/db.secret"},
Resources: []map[string]any{{
"id": "dbenv", "type": "file", "path": "/var/lib/keycloak/database.env",
"mode": "0600",
"content": "KC_DB_URL=jdbc:postgresql://${bound:postgres-database:at}:" +
"${bound:postgres-database:port}/keycloak\n" +
"KC_DB_USERNAME=${bound:postgres-database:as}\n" +
"KC_DB_PASSWORD=${secret:postgres-database}\n",
}},
}},
Needs: []mcat.Needed{{
Name: "postgres-database", From: "anchor", At: "anchor.internal", For: "keycloak",
Sealed: "sealed-db", Serves: map[string]any{"port": float64(5432)},
}},
}
out, err := Declaration(r, mcat.Rendering{})
if err != nil {
t.Fatal(err)
}
file := fileNamed(out, "keycloak.dbenv")
if file == nil {
t.Fatalf("no file in %v", out)
}
got := file["content"].(string)
want := "KC_DB_URL=jdbc:postgresql://anchor.internal:5432/keycloak\n" +
"KC_DB_USERNAME=mesh_workstation_keycloak\n" +
"KC_DB_PASSWORD=${secret:postgres-database}\n"
if got != want {
t.Errorf("the consumer cannot connect with what it was given:\n got %q\nwant %q", got, want)
}
// The password is still a hole. Only the host may fill that one, and only on the machine.
if sealed, _ := file["secrets"].(map[string]any); sealed["postgres-database"] != "sealed-db" {
t.Errorf("the credential did not travel with the file: %v", file)
}
}
// A provider answered on its consumer's own machine is announced at the port the machine PUBLISHES
// it on, not the port the module declared (novox/hq 04-ISSUES/038).
//
// Issue 018 made the same-node provider announced at all; this is the promise after it — that the
// port a co-located consumer is told is the one actually listening at the address it is handed. The
// mesh assigns a bare `ports` mapping a host port (here the container's 5432 becomes 15432 on this
// machine), and it must announce 15432, or the consumer dials workstation.internal:5432 where the
// provider is not published. Cross-node consumers already got this (the plan re-derives the served
// facts with the provider's assignment); the same-node need was settled before the assignment and
// carried the declared port until now.
func TestASameNodeProviderIsAnnouncedAtThePortItIsPublishedOn(t *testing.T) {
provider := mcat.Manifest{
Module: "postgres", Version: "1",
Provides: mcat.FromAnywhere("postgres-database"),
Listens: []mcat.Listening{{Port: 5432, Protocol: "tcp", From: "mesh"}},
Serves: map[string]map[string]any{"postgres-database": {"port": 5432}},
Resources: []map[string]any{{
"id": "server", "type": "container", "name": "postgres",
"ports": []any{"5432"},
}},
}
consumer := mcat.Manifest{
Module: "keycloak", Version: "1", Requires: []string{"postgres-database"},
Resources: []map[string]any{{
"id": "dbenv", "type": "file", "path": "/var/lib/keycloak/database.env", "mode": "0600",
"content": "KC_DB_URL=jdbc:postgresql://${bound:postgres-database:at}:" +
"${bound:postgres-database:port}/keycloak\n",
}},
}
r, err := Resolve(shelf(provider, consumer),
[]string{"postgres", "keycloak"}, reachable(), mcat.World{})
if err != nil {
t.Fatal(err)
}
// The mesh assigned the container's 5432 to 15432 on this machine.
out, err := Declaration(r, mcat.Rendering{Ports: map[string]map[int]int{"postgres": {5432: 15432}}})
if err != nil {
t.Fatal(err)
}
// Where the provider is actually published: the assigned host port, on the machine at large.
server := fileNamed(out, "postgres.server")
if server == nil {
t.Fatalf("the provider container is not in the declaration: %v", out)
}
if published := fmt.Sprint(server["ports"]); !strings.Contains(published, "15432:5432") {
t.Fatalf("the provider was not published on the assigned port: %v", server["ports"])
}
// What the consumer was told to dial: the SAME port, at the announced address — not the 5432 the
// module declared, which is closed on that address.
dbenv := fileNamed(out, "keycloak.dbenv")
if dbenv == nil {
t.Fatalf("the consumer's binding file is not in the declaration: %v", out)
}
got := dbenv["content"].(string)
want := "KC_DB_URL=jdbc:postgresql://workstation.internal:15432/keycloak\n"
if got != want {
t.Errorf("the consumer dials a port the provider does not listen on:\n got %q\nwant %q",
got, want)
}
}
// A port is 5432, not 5432.000000 — which is what a number decoded from JSON would write, and what
// every connection string in the world would refuse.
func TestAPortIsWrittenAsAPort(t *testing.T) {
if got := plainly(float64(5432)); got != "5432" {
t.Errorf("a port was rendered as %q", got)
}
if got := plainly("postgres"); got != "postgres" {
t.Errorf("a string was rendered as %q", got)
}
}
// The name the provider will create is the name the consumer is told to present. One derivation,
// so the two agree by construction rather than by two conventions written on the same day.
func TestTheProviderAndTheConsumerAgreeOnTheName(t *testing.T) {
provider, err := Resolve(shelf(mcat.Manifest{
Module: "postgres", Version: "1", Provides: mcat.FromAnywhere("postgres-database"),
Grants: map[string]string{"postgres-database": "/var/lib/postgres/grants"},
Receives: map[string]string{"postgres-database": "/var/lib/postgres/grants/mesh.json"},
}), []string{"postgres"}, reachable(), mcat.World{})
if err != nil {
t.Fatal(err)
}
out, err := Declaration(provider, mcat.Rendering{Grants: []mcat.Grant{{
Provision: "postgres-database", Consumer: "workstation", From: "keycloak",
Values: map[string]any{"name": "keycloak"}, Sealed: "sealed",
}}})
if err != nil {
t.Fatal(err)
}
var told string
for _, res := range out {
if res["path"] == "/var/lib/postgres/grants/mesh.json" {
told = res["content"].(string)
}
}
if !strings.Contains(told, `"as": "mesh_workstation_keycloak"`) {
t.Errorf("the provider was not told what to call the login: %s", told)
}
if told == "" {
t.Fatal("the provider was told nothing at all")
}
// Which is exactly what the consumer, on its own machine, is told to present.
if mcat.ConsumerIdentity("workstation", "keycloak") != "mesh_workstation_keycloak" {
t.Errorf("got %q", mcat.ConsumerIdentity("workstation", "keycloak"))
}
}
// A binding the module does not have is a typo, refused where it was written.
func TestAFileCannotNameABindingTheModuleDoesNotHave(t *testing.T) {
r := mcat.Resolution{
Node: "workstation",
Modules: []mcat.Manifest{{
Module: "keycloak", Requires: []string{"postgres-database"},
Resources: []map[string]any{{
"id": "conf", "type": "file", "path": "/etc/keycloak.conf",
"content": "host=${bound:s3-bucket:at}\n",
}},
}},
Needs: []mcat.Needed{{Name: "postgres-database", From: "anchor", At: "a", For: "keycloak"}},
}
_, err := Declaration(r, mcat.Rendering{})
if err == nil {
t.Fatal("a module read a binding it does not have")
}
if !strings.Contains(err.Error(), "postgres-database") {
t.Errorf("the refusal does not say what it could have named: %v", err)
}
}
// A key the provider does not serve is refused too. Written through, the literal placeholder
// becomes a hostname, and the failure names neither the module nor the mesh.
func TestAKeyTheProviderDoesNotServeIsRefused(t *testing.T) {
r := mcat.Resolution{
Node: "workstation",
Modules: []mcat.Manifest{{
Module: "keycloak", Requires: []string{"postgres-database"},
Resources: []map[string]any{{
"id": "conf", "type": "file", "path": "/etc/keycloak.conf",
"content": "sock=${bound:postgres-database:socket}\n",
}},
}},
Needs: []mcat.Needed{{
Name: "postgres-database", From: "anchor", At: "a", For: "keycloak",
Serves: map[string]any{"port": float64(5432)},
}},
}
_, err := Declaration(r, mcat.Rendering{})
if err == nil {
t.Fatal("a module read a key nothing serves, and the placeholder became a value")
}
if !strings.Contains(err.Error(), `"port"`) {
t.Errorf("the refusal does not say what is served: %v", err)
}
}
// A machine's resolver configuration must name its resolver by address — it cannot resolve the name
// of the thing it resolves names with (novox/hq ADR 0194). So a binding offers the providing
// machine's private address beside its name, and only when the machine has one.
func TestABindingOffersTheProvidersAddress(t *testing.T) {
consumer := func() mcat.Resolution {
return mcat.Resolution{
Node: "workstation",
Modules: []mcat.Manifest{{
Module: "resolv-conf",
Requires: []string{"wildcard-resolution"},
Resources: []map[string]any{{
"id": "resolv", "type": "file", "path": "/etc/resolv.conf", "mode": "0644",
"content": "nameserver ${bound:wildcard-resolution:address}\n",
}},
}},
Needs: []mcat.Needed{{Name: "wildcard-resolution", From: "anchor", At: "anchor.internal", For: "resolv-conf"}},
}
}
out, err := Declaration(consumer(), mcat.Rendering{Machines: map[string]string{"anchor.internal": "10.77.0.1"}})
if err != nil {
t.Fatal(err)
}
if got := fileNamed(out, "resolv-conf.resolv")["content"]; got != "nameserver 10.77.0.1\n" {
t.Fatalf("the resolver is not named by its address: %q", got)
}
// A machine with no address yet: refused, never written with a blank where the address belongs.
if _, err := Declaration(consumer(), mcat.Rendering{}); err == nil {
t.Fatal("a file naming an address the mesh does not have was composed")
}
}