Files
mesh-controller/examples/modules/modules_test.go
T
jschoubben 122680b554 A consumer can write its own connection string
novox/hq 04-ISSUES/023. A consumer was given its password, the address,
the port and where its credential lives, and still could not connect —
the user name was invented by the provisioner and recorded nowhere, and
the rest sat in a JSON binding that a program reading KEY=value cannot
use.

Both halves have the same cause: the mesh knew something and did not say
it.

**Who a consumer is, said once.** The provisioner used to derive
mesh_<node>_<module> and that string existed nowhere else — not in the
control plane, not in the binding, and above all not at the consumer,
which has to present it. Now the mesh derives it once and sends it to
both ends, so they agree by construction rather than by two conventions
that were the same on the day they were written. The provisioners refuse
to invent one if the mesh says nothing, because falling back to a name
of their own would create a role the consumer would never guess and
everything would report success.

**Bound values reach the file that needs them.** ${bound:provision:key}
is the symmetric twin of the sealed placeholder, and simpler: these
values are not secret, so the control plane fills them in before sending
and the host gains no field and learns no format. It stays
name-agnostic — at, as and from are true of any provision, and every
other key comes from what the provider said it serves.

The asymmetry it removes was backwards. The secret is the hard case,
because the mesh must not be able to read it, and the secret was the
part that already arrived.

Keycloak and Gitea now produce complete connections, asserted from the
manifests on disk rather than from fixtures: every part filled, no
placeholder surviving as a value, and the password still a hole only the
host can close. Three faults injected, each caught.
2026-09-01 03:03:07 +02:00

422 lines
15 KiB
Go

package modules
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"github.com/novox/mesh-control/internal/catalogue"
"github.com/novox/mesh-control/internal/overlay"
)
// The examples are manifests, so the thing to check is that the catalogue accepts them.
//
// A manifest that only ever appears in a document is a manifest nobody has run through the parser,
// and the parser refuses unknown keys — so a typo here would be discovered by whoever first tried
// to use one, which is the opposite of what an example is for.
func read(t *testing.T, name string) catalogue.Manifest {
t.Helper()
raw, err := os.ReadFile(filepath.Join(".", name))
if err != nil {
t.Fatal(err)
}
m, err := catalogue.ParseManifest(raw)
if err != nil {
t.Fatalf("%s is not a manifest this mesh accepts: %v", name, err)
}
return m
}
func TestEveryExampleIsAManifestTheMeshAccepts(t *testing.T) {
found, err := filepath.Glob("*.json")
if err != nil {
t.Fatal(err)
}
if len(found) == 0 {
t.Fatal("no examples, so this test proves nothing")
}
for _, name := range found {
read(t, name)
}
}
// The serving module reads what the mesh writes, and restarts when the mesh rewrites it.
//
// Without the second it would serve the names it started with for ever — every machine that
// joined afterwards unreachable by name, and every check passing.
func TestTheResolverReadsTheMeshsNamesAndFollowsThem(t *testing.T) {
m := read(t, "dnsmasq.json")
var config, service map[string]any
for _, r := range m.Resources {
switch r["id"] {
case "config":
config = r
case "service":
service = r
}
}
if config == nil || service == nil {
t.Fatal("the module has no configuration or no service")
}
if !strings.Contains(config["content"].(string), overlay.ResolverPath) {
t.Fatalf("it does not read what the mesh writes at %s", overlay.ResolverPath)
}
var follows bool
for _, id := range service["restart-on"].([]any) {
if id.(string) == overlay.Resolver+".nodes" {
follows = true
}
}
if !follows {
t.Fatalf("it does not restart when the mesh rewrites the names: %v", service["restart-on"])
}
}
// It binds names the mesh chose, so it needs to know nothing about the machine it is on.
//
// That is the whole reason these can be static manifests: a resolver must bind somewhere and a
// stub must be pointed somewhere, and neither address is knowable in advance — unless the mesh
// named it.
func TestTheResolverNeedsToKnowNothingAboutItsMachine(t *testing.T) {
config := read(t, "dnsmasq.json").Resources[1]["content"].(string)
// The directive, not the word: the comment above it names the interface too, so a plain
// Contains passes whatever the module actually binds. It did.
if !strings.Contains(config, "interface="+overlay.Interface+"\n") {
t.Fatalf("it does not bind the private network's interface %q:\n%s",
overlay.Interface, config)
}
// That it binds one, not which. Which address it is belongs in the manifests, where the
// asking modules can be checked against it — naming it here too would be a fourth place to
// keep in step, and the one nobody would think to change.
if !strings.Contains(config, "\nlisten-address=127.0.0.") {
t.Fatalf("it answers on no address for the machine's own use:\n%s", config)
}
// Not an address that belongs to something else.
//
// **systemd-resolved holds .53 AND .54** — the stub and the proxy stub. This module asserted
// .54 was free, in a comment that read as reasoned, and a machine said otherwise: dnsmasq
// could not start at all. A unit test cannot know which addresses a machine has spare, but it
// can hold on to what one has already told us.
for _, taken := range []string{"127.0.0.1", "127.0.0.53", "127.0.0.54"} {
if strings.Contains(config, "listen-address="+taken) {
t.Fatalf("it takes %s, which belongs to something else:\n%s", taken, config)
}
}
}
// Everything that points resolution at the mesh points at the same place.
//
// Three files name this address — one binds it and two send queries to it — and a change to one
// of them alone is a resolver answering where nobody asks.
func TestTheAskingModulesPointAtWhereTheResolverAnswers(t *testing.T) {
serving := read(t, "dnsmasq.json").Resources[1]["content"].(string)
var at string
for _, line := range strings.Split(serving, "\n") {
if rest, found := strings.CutPrefix(strings.TrimSpace(line), "listen-address="); found {
at = rest
}
}
if at == "" {
t.Fatal("the resolver binds no address for the machine's own use")
}
for _, asking := range []string{"resolved-split-dns.json", "resolv-conf.json"} {
m := read(t, asking)
var mentions bool
for _, r := range m.Resources {
if content, ok := r["content"].(string); ok && strings.Contains(content, at) {
mentions = true
}
}
if !mentions {
t.Fatalf("%s does not point at %s, where the resolver answers", asking, at)
}
}
}
// The two ways of deciding what a machine asks claim the same thing, so the mesh refuses the pair.
func TestTwoWaysOfOwningTheResolverCannotBothBeAssigned(t *testing.T) {
shelf := map[string]catalogue.Manifest{}
for _, name := range []string{"resolved-split-dns.json", "resolv-conf.json", "dnsmasq.json"} {
m := read(t, name)
shelf[m.Module] = m
}
// Something has to answer `wildcard-resolution`, or they are refused for that instead and the
// test would pass without ever reaching the claim.
shelf["dnsmasq"] = read(t, "dnsmasq.json")
_, err := catalogue.Resolve(shelf,
[]string{"dnsmasq", "resolved-split-dns", "resolv-conf"},
catalogue.Node{Name: "anchor", Capabilities: map[string]bool{}},
catalogue.World{Unchecked: true})
if err == nil {
t.Fatal("both ways of owning the resolver were assigned to one machine")
}
said := err.Error()
if !strings.Contains(said, "the-resolver-configuration") {
t.Fatalf("the refusal does not name what they both want: %v", said)
}
}
// And the two roles are not the same claim: a machine runs one resolver AND one thing deciding
// what it asks, so serving and asking must be assignable together.
func TestServingAndAskingAreAssignableTogether(t *testing.T) {
shelf := map[string]catalogue.Manifest{}
for _, name := range []string{"dnsmasq.json", "resolved-split-dns.json"} {
m := read(t, name)
shelf[m.Module] = m
}
if _, err := catalogue.Resolve(shelf,
[]string{"dnsmasq", "resolved-split-dns"},
catalogue.Node{Name: "anchor", Capabilities: map[string]bool{}},
catalogue.World{Unchecked: true}); err != nil {
t.Fatalf("a resolver and the thing pointing at it cannot both be assigned: %v", err)
}
}
// The examples are JSON a person edits, so a stray comma is worth catching here rather than on a
// machine.
func TestTheExamplesAreWellFormed(t *testing.T) {
found, _ := filepath.Glob("*.json")
for _, name := range found {
raw, err := os.ReadFile(name)
if err != nil {
t.Fatal(err)
}
var any map[string]any
if err := json.Unmarshal(raw, &any); err != nil {
t.Fatalf("%s is not JSON: %v", name, err)
}
}
}
// The resolver must not look up its own upstreams.
//
// Whatever points a machine at the mesh writes that address into resolv.conf, so a resolver that
// read it would find itself — and every query it could not answer locally would loop until its
// receive queue filled. It did: 15KB of queries backed up and every lookup on the machine hung.
//
// It needs no upstream because it is never asked for anything else: the asking module routes only
// the mesh's suffix here and leaves the rest where the machine already sent it.
func TestTheResolverDoesNotAskItselfForUpstreams(t *testing.T) {
config := read(t, "dnsmasq.json").Resources[1]["content"].(string)
if !strings.Contains(config, "\nno-resolv\n") {
t.Fatalf("it reads resolv.conf for upstreams, which now points at itself:\n%s", config)
}
// And names no upstream of its own: choosing one would send every query this machine cannot
// answer somewhere nobody agreed to.
for _, line := range strings.Split(config, "\n") {
if strings.HasPrefix(strings.TrimSpace(line), "server=") {
t.Fatalf("it forwards to %q, which is not the mesh's to choose", line)
}
}
}
// The two halves of an object-store edge, as a pair.
//
// A provider and a consumer that only ever appear separately are two manifests nobody has checked
// against each other: the name one provides has to be the name the other requires, and the key a
// consumer contributes has to be the one the provisioner reads. Both were got wrong while writing
// them, and neither would have been caught by parsing either file alone.
func TestTheObjectStoreEdgeFitsTogether(t *testing.T) {
provider := read(t, "object-store.json")
consumer := read(t, "photos.json")
const provision = "s3-bucket"
var provides bool
for _, offer := range provider.Provides {
if offer.Name == provision {
provides = true
}
}
if !provides {
t.Fatalf("the provider does not offer %q", provision)
}
if !strings.Contains(strings.Join(consumer.Requires, ","), provision) {
t.Fatalf("the consumer does not require %q", provision)
}
// Where each side wants to be told. A provider that receives nowhere is a provider the mesh
// writes nothing for, and a provisioner with nothing to read.
if provider.Receives[provision] == "" {
t.Error("the provider says nowhere to write what its consumers asked for")
}
if provider.Grants[provision] == "" {
t.Error("the provider says nowhere to write its consumers' credentials")
}
if consumer.Binds[provision] == "" {
t.Error("the consumer says nowhere to be told where its bucket is")
}
if consumer.Secrets[provision] == "" {
t.Error("the consumer says nowhere to be given its key")
}
// The key the provisioner reads out of `values`. It looks for `bucket`, so a consumer
// contributing `name` — which is what the database one contributes — resolves cleanly and
// then fails on the machine with "asked for a bucket and did not name it".
if _, named := consumer.Contributes[provision]["bucket"]; !named {
t.Errorf("the consumer contributes %v, and the provisioner reads \"bucket\"",
consumer.Contributes[provision])
}
}
// Every hole an example leaves for a credential can be filled from what that module declared.
//
// **A manifest that parses is not a manifest that works.** These say `${secret:x}` in a file and
// declare `x` under `own-secrets`; if the two ever disagree the mesh refuses the whole declaration
// at push time, on the machine, with the module's name and nothing else to go on. Checking it here
// costs nothing and moves the answer to whoever edited the file.
//
// This is also the shape that was missing entirely until 2026-09-01: an own secret arrives as a
// file whose whole content is the password, and every one of these programs reads `KEY=value`. The
// manifests said `own-secrets` pointed at a `.env` and it did not — it pointed at a password.
func TestEveryCredentialHoleCanBeFilledByTheModuleThatLeftIt(t *testing.T) {
found, err := filepath.Glob("*.json")
if err != nil {
t.Fatal(err)
}
var checked int
for _, name := range found {
m := read(t, name)
has := map[string]bool{}
for own := range m.OwnSecrets {
has[own] = true
}
for required := range m.Secrets {
has[required] = true
}
for _, r := range m.Resources {
content, ok := r["content"].(string)
if !ok {
continue
}
for _, wanted := range secretsUsedForTest(content) {
checked++
if !has[wanted] {
t.Errorf(
"%s: %v says ${secret:%s}, and %s neither owns a secret by that name "+
"nor requires anything that grants one",
name, r["id"], wanted, m.Module)
}
}
}
}
if checked == 0 {
t.Fatal("no example puts a credential into a file, so this test proves nothing")
}
}
// A secret file is a password and nothing else, so nothing may read one as an env file.
//
// The fault this catches is the one these manifests shipped with: `own-secrets` pointing at a
// path called `.env`, mounted as `env-file`, holding a bare password. Docker reads that as a
// malformed line and the container starts with no password at all.
func TestNoContainerReadsABarePasswordAsAnEnvFile(t *testing.T) {
found, _ := filepath.Glob("*.json")
for _, name := range found {
m := read(t, name)
bare := map[string]bool{}
for _, where := range m.OwnSecrets {
bare[where] = true
}
for _, where := range m.Secrets {
bare[where] = true
}
for _, r := range m.Resources {
files, ok := r["env-file"].([]any)
if !ok {
continue
}
for _, f := range files {
if bare[fmt.Sprint(f)] {
t.Errorf(
"%s: %v reads %s as an env file, and that path holds a bare password — "+
"declare a file whose content says ${secret:...} and read that instead",
name, r["id"], f)
}
}
}
}
}
// The same expression the control plane and the host both match.
var placeholder = regexp.MustCompile(`\$\{secret:([a-z0-9][a-z0-9-]*)\}`)
func secretsUsedForTest(content string) []string {
var used []string
seen := map[string]bool{}
for _, m := range placeholder.FindAllStringSubmatch(content, -1) {
if !seen[m[1]] {
seen[m[1]] = true
used = append(used, m[1])
}
}
return used
}
// The real manifests, resolved together, produce a connection a program could use.
//
// **Parsing is not working, and this file has now learned that twice.** These modules parsed and
// resolved for a day while their credentials went into files nothing could read; they would have
// parsed and resolved just as happily with a connection string that named no user. What has to be
// true is that the bytes reaching the machine are usable, so that is what this asks.
func TestKeycloakGetsAConnectionAProgramCouldUse(t *testing.T) {
shelf := map[string]catalogue.Manifest{}
for _, name := range []string{"postgres.json", "keycloak.json"} {
m := read(t, name)
shelf[m.Module] = m
}
resolved, err := catalogue.Resolve(shelf, []string{"keycloak"},
catalogue.Node{Name: "workstation", At: "workstation.internal",
Capabilities: map[string]bool{"container-runtime": true}},
catalogue.World{Offered: map[string][]catalogue.Provider{
"postgres-database": {{Node: "anchor", At: "anchor.internal",
Serves: map[string]any{"port": float64(5432)}}},
}})
if err != nil {
t.Fatalf("the real manifests do not resolve: %v", err)
}
for i := range resolved.Needs {
resolved.Needs[i].Sealed = "sealed"
}
out, err := resolved.Declaration(catalogue.Rendering{
Needed: map[string]map[string]string{"keycloak": {"admin": "sealed-admin"}},
})
if err != nil {
t.Fatalf("the real manifests do not declare: %v", err)
}
var env string
for _, r := range out {
if r["path"] == "/var/lib/keycloak/database.env" {
env, _ = r["content"].(string)
}
}
if env == "" {
t.Fatal("keycloak was given no database configuration at all")
}
// Every part of a connection, and nothing left unfilled. A leftover ${...} would be read as
// a value by whatever parses this.
for _, wanted := range []string{
"KC_DB_URL=jdbc:postgresql://anchor.internal:5432/keycloak",
"KC_DB_USERNAME=mesh_workstation_keycloak",
} {
if !strings.Contains(env, wanted) {
t.Errorf("the connection is missing %q:\n%s", wanted, env)
}
}
if strings.Contains(env, "${bound:") {
t.Errorf("a placeholder reached the machine as a value:\n%s", env)
}
// The password is the one hole that stays, because only the host may fill it.
if !strings.Contains(env, "KC_DB_PASSWORD=${secret:postgres-database}") {
t.Errorf("the password is not left for the host to fill:\n%s", env)
}
}