Files
mesh-controller/internal/catalogue/contributes_test.go
jschoubben d0ef659824 fix: a require-only consumer of a parameterless provision still asks (and is minted a credential)
A consumer that requires a provision whose serves names no consumer key (redis-cache, amqp)
contributes no payload, but it still ASKS for it. ContributionsFrom keyed 'asks' on
contributions alone, so such a consumer's grant got From='' — read as withdrawn — and the
provider never created its account. redis-cache consumers (e.g. baserow) were silently
unprovisioned, tolerated only by their embedded fallback. A module asks iff it still requires
the provision, whether or not it hands anything up. Regression test added.

Found by the lavinmq AMQP provider bed (given:[] for a require-only amqp consumer); fix
lab-proven green there.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-06 23:20:58 +02:00

392 lines
14 KiB
Go

package catalogue
import (
"encoding/json"
"strings"
"testing"
)
// The other half of an edge.
//
// `requires` says a thing must be there. It never said what to do with it — a web application
// requiring a reverse proxy has to say *which name, which port*, and there was nowhere to put
// that. The two modules that needed it most, the proxy and the VPN, went round the outside and
// opened a connection to the control plane's database, which is why every node held a credential
// to it permanently.
func published(module, host string, port int) Manifest {
return Manifest{Module: module, Version: "1",
Contributes: map[string]map[string]any{
"reverse-proxy": {"host": host, "port": port},
}}
}
func proxy() Manifest {
return Manifest{Module: "traefik", Version: "1",
Provides: Offers("reverse-proxy"),
Receives: map[string]string{"reverse-proxy": "/etc/traefik/mesh.json"},
Resources: []map[string]any{
{"id": "up", "type": "service", "unit": "traefik", "state": "running",
"restart-on": []any{ReceivedID("reverse-proxy")}},
}}
}
// received digs the delivered contributions back out of a declaration.
func received(t *testing.T, out []map[string]any) []Contribution {
t.Helper()
for _, r := range out {
if r["path"] != "/etc/traefik/mesh.json" {
continue
}
body := r["content"].(string)
var parsed struct {
Requirement string `json:"requirement"`
Given []Contribution `json:"given"`
}
if err := json.Unmarshal([]byte(body), &parsed); err != nil {
t.Fatalf("the file the proxy is given is not readable: %v\n%s", err, body)
}
if parsed.Requirement != "reverse-proxy" {
t.Fatalf("the file does not say what it is about: %q", parsed.Requirement)
}
return parsed.Given
}
t.Fatalf("the provider was given no file at all: %v", out)
return nil
}
func TestTheFileTheProviderGetsIsMachineReadable(t *testing.T) {
// It is written for a program, and the first version put a `//` header above the JSON — a
// file that says "do not edit" to a person and fails to parse for the thing meant to read it.
got, _ := Resolve(shelf(proxy(), published("board", "board", 8080)), []string{"board"}, workstation(), World{})
for _, r := range mustDeclare(t, got) {
if r["path"] != "/etc/traefik/mesh.json" {
continue
}
var any map[string]any
if err := json.Unmarshal([]byte(r["content"].(string)), &any); err != nil {
t.Fatalf("the file is not parseable: %v\n%s", err, r["content"])
}
if note, _ := any["generated"].(string); !strings.Contains(note, "do not edit") {
// Inside the document rather than above it, so it reaches a person without
// breaking the parse.
t.Fatalf("the file does not say it is generated: %v", any["generated"])
}
return
}
t.Fatal("no received file")
}
func TestAModuleTellsItsProviderWhatItNeeds(t *testing.T) {
got, err := Resolve(shelf(proxy(), published("board", "board", 8080)), []string{"board"}, workstation(), World{})
if err != nil {
t.Fatal(err)
}
out, err := got.Declaration(Rendering{})
if err != nil {
t.Fatal(err)
}
given := received(t, out)
if len(given) != 1 || given[0].From != "board" {
t.Fatalf("the proxy was not told about board: %v", given)
}
if given[0].Values["host"] != "board" || given[0].Values["port"] != float64(8080) {
t.Fatalf("the contribution did not survive: %v", given[0].Values)
}
}
func TestContributingToSomethingIsRequiringIt(t *testing.T) {
// Asking to be published means a publisher must exist. A module that had to say both would
// eventually say one, and the failure would be a machine where nothing serves the route.
got, err := Resolve(shelf(proxy(), published("board", "board", 8080)), []string{"board"}, workstation(), World{})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(strings.Join(names(got), " "), "traefik") {
t.Fatalf("contributing to reverse-proxy did not bring one in: %v", names(got))
}
}
func TestNothingToContributeToIsRefused(t *testing.T) {
_, err := Resolve(shelf(published("board", "board", 8080)), []string{"board"}, workstation(), World{})
if err == nil {
t.Fatal("a module was published through a proxy that does not exist")
}
if !strings.Contains(err.Error(), "reverse-proxy") {
t.Fatalf("the refusal does not name what is missing: %v", err)
}
}
func TestEveryPublisherOnTheMachineIsInOneFile(t *testing.T) {
got, err := Resolve(shelf(proxy(),
published("board", "board", 8080),
published("archive", "archive", 9000),
), []string{"board", "archive"}, workstation(), World{})
if err != nil {
t.Fatal(err)
}
given := received(t, mustDeclare(t, got))
if len(given) != 2 {
t.Fatalf("the proxy was told about %d of 2: %v", len(given), given)
}
// Ordered by module, because this becomes a file and a file whose lines move about looks
// changed when nothing changed — which would restart the proxy for ever.
if given[0].From != "archive" || given[1].From != "board" {
t.Fatalf("the order is not stable: %v", given)
}
}
func TestAProviderWithNoConsumersStillGetsTheFile(t *testing.T) {
// Empty rather than absent. A provider that finds no file cannot tell "nothing asked for me"
// from "the mesh never wrote it", and those want completely different responses — the same
// rule the host follows about a service that does not exist.
got, err := Resolve(shelf(proxy()), []string{"traefik"}, workstation(), World{})
if err != nil {
t.Fatal(err)
}
if given := received(t, mustDeclare(t, got)); len(given) != 0 {
t.Fatalf("got %v", given)
}
}
func TestTheProviderCanReloadWhenTheRoutesChange(t *testing.T) {
// A proxy that got a new route and did not reload is a route that silently does not work.
// The same fault the private network had when a peer list changed under a running interface,
// which is why the received file has a name a module can point at.
got, _ := Resolve(shelf(proxy(), published("board", "board", 8080)), []string{"board"}, workstation(), World{})
out := mustDeclare(t, got)
for _, r := range out {
if r["type"] != "service" {
continue
}
reflects, ok := r["restart-on"].([]any)
if !ok || len(reflects) != 1 {
t.Fatalf("the proxy does not reflect anything: %v", r)
}
if reflects[0] != "traefik."+ReceivedID("reverse-proxy") {
t.Fatalf("it reflects %v, which is not the file it was given", reflects[0])
}
return
}
t.Fatal("no service in the declaration")
}
func TestARouteCanBeSetPerMesh(t *testing.T) {
// The hostname is exactly the kind of thing that differs between one mesh and the next. A
// module whose route could not be set would have to be edited to be reused anywhere.
got, _ := Resolve(shelf(proxy(), published("board", "board", 8080)), []string{"board"}, workstation(), World{})
out, err := got.Declaration(Rendering{Settings: SettingsBy{
"board": {{From: "the mesh", Values: map[string]any{"host": "dashboard"}}},
}})
if err != nil {
t.Fatal(err)
}
given := received(t, out)
if given[0].Values["host"] != "dashboard" {
t.Fatalf("the setting did not reach the route: %v", given[0].Values)
}
if given[0].Values["port"] != float64(8080) {
t.Fatalf("setting the host dropped the port: %v", given[0].Values)
}
}
func TestReceivingWhatYouDoNotProvideIsRefused(t *testing.T) {
// It would create a file nobody ever writes to, on a machine where nothing asked for it.
_, err := ParseManifest([]byte(`{"module":"traefik","version":"1",
"receives":{"reverse-proxy":"/etc/traefik/mesh.json"}}`))
if err == nil {
t.Fatal("a module received contributions to something it does not provide")
}
if !strings.Contains(err.Error(), "does not provide") {
t.Fatalf("unhelpful refusal: %v", err)
}
}
func TestAnEmptyContributionIsRefused(t *testing.T) {
// Either a mistake or a requirement written the long way round, and both are better said.
_, err := ParseManifest([]byte(`{"module":"board","version":"1",
"contributes":{"reverse-proxy":{}}}`))
if err == nil {
t.Fatal("a module contributed nothing and was accepted")
}
if !strings.Contains(err.Error(), "require it") {
t.Fatalf("the refusal does not say what to do instead: %v", err)
}
}
// A provision answered from anywhere in the mesh has consumers on other machines, and the
// provider has to know who they are. Contributions were node-local until this, which meant the
// one case that most needed them was the one they did not reach.
func provider() Manifest {
return Manifest{Module: "postgres", Version: "1",
Provides: FromAnywhere("postgres-database"),
Receives: map[string]string{"postgres-database": "/var/lib/postgres/grants/mesh.json"},
Grants: map[string]string{"postgres-database": "/var/lib/postgres/grants"},
}
}
// oneGrant is a declaration with a single consumer on another machine.
func oneGrant(t *testing.T) []map[string]any {
t.Helper()
got, err := Resolve(shelf(provider()), []string{"postgres"}, reachable(), World{})
if err != nil {
t.Fatal(err)
}
out, err := got.Declaration(Rendering{Grants: []Grant{{
Provision: "postgres-database", Consumer: "workstation", From: "meshboard",
Values: map[string]any{"name": "meshboard"}, Sealed: "c2VhbGVk",
}}})
if err != nil {
t.Fatal(err)
}
return out
}
func grantedTo(t *testing.T, out []map[string]any) []Contribution {
t.Helper()
for _, r := range out {
if r["path"] != "/var/lib/postgres/grants/mesh.json" {
continue
}
var parsed struct {
Given []Contribution `json:"given"`
}
if err := json.Unmarshal([]byte(r["content"].(string)), &parsed); err != nil {
t.Fatal(err)
}
return parsed.Given
}
t.Fatalf("the provider was given no manifest: %v", out)
return nil
}
func TestAProviderIsToldAboutConsumersOnOtherMachines(t *testing.T) {
// A database told to create a password and not who for can do nothing with it.
given := grantedTo(t, oneGrant(t))
if len(given) != 1 {
t.Fatalf("got %v", given)
}
if given[0].Node != "workstation" || given[0].From != "meshboard" {
t.Fatalf("it does not say who asked: %v", given[0])
}
if given[0].Values["name"] != "meshboard" {
t.Fatalf("it does not say what was asked for: %v", given[0].Values)
}
}
func TestTheManifestNamesTheFileRatherThanCarryingTheCredential(t *testing.T) {
// The mesh discarded the value and could not put it here if it wanted to. What is here is
// where to find it — and the readable half therefore stays readable.
out := oneGrant(t)
given := grantedTo(t, out)
// Named after the machine and the module, because a consumer is both (novox/hq
// 04-ISSUES/022). The machine alone, two modules on one node wrote to one path.
if given[0].Secret != "/var/lib/postgres/grants/workstation.meshboard.secret" {
t.Fatalf("the manifest does not name the credential's file: %q", given[0].Secret)
}
for _, r := range out {
if r["path"] != "/var/lib/postgres/grants/mesh.json" {
continue
}
if strings.Contains(r["content"].(string), "c2VhbGVk") {
t.Fatal("the readable manifest carries the sealed credential")
}
}
}
func TestTheCredentialItselfLandsSealedBesideIt(t *testing.T) {
for _, r := range oneGrant(t) {
if r["path"] != "/var/lib/postgres/grants/workstation.meshboard.secret" {
continue
}
if r["sealed"] != "c2VhbGVk" {
t.Fatalf("got %v", r)
}
if r["content"] != nil {
t.Fatal("a credential was written in the clear")
}
return
}
t.Fatal("no credential file")
}
func TestAConsumersOwnContributionsAreStillThere(t *testing.T) {
// Cross-node grants are merged in with this machine's own, because from the provider's side
// they are the same thing — somebody wanting something — and a provider that had to read two
// lists would read one of them.
got, err := Resolve(shelf(proxy(), published("board", "board", 8080)),
[]string{"board"}, reachable(), World{})
if err != nil {
t.Fatal(err)
}
given := received(t, mustDeclare(t, got))
if len(given) != 1 || given[0].Node != "" {
t.Fatalf("a local contribution grew a node: %v", given)
}
}
func TestARequireOnlyConsumerOfAParameterlessProvisionStillAsks(t *testing.T) {
// A consumer that requires a parameterless provision (one whose serves names no consumer key —
// redis-cache, amqp) contributes no payload, but it still ASKS for the provision and must be
// granted a credential. Keying "asks" on contributions alone gave its grant From="" — read as
// withdrawn — so the provider never created the account and the consumer authenticated nowhere.
r := Resolution{
Node: "laptop",
Modules: []Manifest{{
Module: "amqp-ping",
Requires: []string{"amqp"},
}},
}
values, asks, err := r.ContributionsFrom("amqp", "amqp-ping", SettingsBy{})
if err != nil {
t.Fatal(err)
}
if !asks {
t.Fatal("a require-only consumer was treated as not asking; its grant would be withdrawn and it would never be provisioned")
}
if values == nil {
t.Fatalf("asks is true but values is nil; expected an empty contribution, got %v", values)
}
}
func TestAConsumerThatStoppedAskingIsWithdrawn(t *testing.T) {
// Withdrawal is how a credential is taken away. The provisioner removes what nobody asks for,
// and it can only do that if the mesh stops asking — a consumer that was unassigned would
// otherwise keep a working login for ever, and nothing would say so.
//
// A grant with no asking module is exactly that state: the mesh still holds the secret,
// because it is sealed and unusable to the mesh anyway, and the machine no longer wants it.
got, err := Resolve(shelf(provider()), []string{"postgres"}, reachable(), World{})
if err != nil {
t.Fatal(err)
}
out, err := got.Declaration(Rendering{Grants: []Grant{
{Provision: "postgres-database", Consumer: "still-here", From: "meshboard",
Values: map[string]any{"name": "meshboard"}, Sealed: "c2VhbGVk"},
{Provision: "postgres-database", Consumer: "gone-away", Sealed: "c3RhbGU="},
}})
if err != nil {
t.Fatal(err)
}
given := grantedTo(t, out)
if len(given) != 1 || given[0].Node != "still-here" {
t.Fatalf("the provider is still told about a machine that stopped asking: %v", given)
}
// And no credential is written for it either, or the provisioner would find a file for a
// consumer its manifest does not mention and have to guess what that means.
for _, r := range out {
if path, _ := r["path"].(string); strings.Contains(path, "gone-away") {
t.Fatalf("a withdrawn consumer's credential is still delivered: %v", r)
}
}
}