novox/hq 04-ISSUES/022. A credential was keyed by provision, consumer node and provider node, so "who is asking" was answered by naming a host. The node this mesh exists to take over runs eight modules against one database server. The symptom had two halves and only one was loud. The provider refused, naming the modules and explaining they would share one credential, which reads as a decision rather than a limit. The consumer did not refuse: it resolved cleanly, wrote one module's credential file and left the others absent — a service that starts and cannot authenticate, with nothing saying why. That is 021 again on a different axis. Three modules wanting one database produced one need, carrying whichever module mentioned it first, because the resolution walk is a work-list over names. The fan-out now happens in one place, after the walk. The record path already did this correctly and said why: a consumer here is a module on a machine. It is the same rule. Downstream: the secret's key gains the consuming module, the grant file is named after both halves, needs are matched by provision and module rather than provision alone, and the provisioners name the role and the access key after the module. The refusal in ContributionsTo is gone because there is nothing left to refuse. Worth stating plainly: without that refusal, gitea's login would have opened keycloak's database. From the provisioner's side it created exactly what it was asked to create. Existing secrets are discarded rather than backfilled. They cannot say which module they were for, and a secret is remade and delivered to both ends on the next push — so this costs one rotation and invents nothing. Also guards the role name against PostgreSQL's 63-byte truncation, which is a notice rather than an error and would reintroduce exactly this collision at a length nobody tests. Three faults injected — the fan-out removed, needs matched by name alone, the grant file named after the machine — each caught.
144 lines
5.3 KiB
Go
144 lines
5.3 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A node runs several modules that all want one database (novox/hq 04-ISSUES/022).
|
|
//
|
|
// **The ordinary arrangement, and it could not be planned at all.** The walk that resolves a node
|
|
// is a work-list over names, so a requirement three modules shared was visited once and produced
|
|
// one need — carrying whichever module mentioned it first. Everything downstream inherited that:
|
|
// one credential, keyed by machine, named after a machine by the provisioner.
|
|
//
|
|
// The symptom had two halves and only one was loud. The provider refused, naming the modules and
|
|
// saying they would share one credential, which reads as a decision. The consumer did not: it
|
|
// resolved cleanly, wrote one module's credential file, and left the other two absent — a service
|
|
// that starts and cannot authenticate, with nothing anywhere saying why. That is the shape of 021
|
|
// again, on a different axis.
|
|
|
|
func threeConsumers() map[string]Manifest {
|
|
return shelf(
|
|
Manifest{Module: "postgres", Version: "1", Provides: FromAnywhere("postgres-database")},
|
|
Manifest{Module: "gitea", Version: "1", Requires: []string{"postgres-database"},
|
|
Contributes: map[string]map[string]any{"postgres-database": {"name": "gitea"}},
|
|
Secrets: map[string]string{"postgres-database": "/var/lib/gitea/db.secret"}},
|
|
Manifest{Module: "keycloak", Version: "1", Requires: []string{"postgres-database"},
|
|
Contributes: map[string]map[string]any{"postgres-database": {"name": "keycloak"}},
|
|
Secrets: map[string]string{"postgres-database": "/var/lib/keycloak/db.secret"}},
|
|
Manifest{Module: "umami", Version: "1", Requires: []string{"postgres-database"},
|
|
Contributes: map[string]map[string]any{"postgres-database": {"name": "umami"}},
|
|
Secrets: map[string]string{"postgres-database": "/var/lib/umami/db.secret"}},
|
|
)
|
|
}
|
|
|
|
func TestEveryConsumerOnANodeGetsItsOwnCredential(t *testing.T) {
|
|
got, err := Resolve(threeConsumers(), []string{"gitea", "keycloak", "umami"},
|
|
reachable(), World{Offered: onNetwork("anchor")})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.Needs) != 3 {
|
|
t.Fatalf("three modules want a database and the node has %d need(s): %v",
|
|
len(got.Needs), got.Needs)
|
|
}
|
|
for _, want := range []string{"gitea", "keycloak", "umami"} {
|
|
var found bool
|
|
for _, n := range got.Needs {
|
|
if n.For == want {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("%s wants a database and no credential is made for it", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Each consumer's own credential reaches its own file. Matching on the provision alone, every
|
|
// consumer took whichever need was last — a module handed somebody else's password, which is a
|
|
// valid credential and therefore fails in a way that looks like a configuration error.
|
|
func TestEachConsumerGetsItsOwnCredentialAndNotAnothersFile(t *testing.T) {
|
|
got, err := Resolve(threeConsumers(), []string{"gitea", "keycloak", "umami"},
|
|
reachable(), World{Offered: onNetwork("anchor")})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i := range got.Needs {
|
|
got.Needs[i].Sealed = "sealed-for-" + got.Needs[i].For
|
|
}
|
|
out, err := got.Declaration(Rendering{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{"gitea", "keycloak", "umami"} {
|
|
var seen bool
|
|
for _, r := range out {
|
|
if r["path"] != "/var/lib/"+want+"/db.secret" {
|
|
continue
|
|
}
|
|
seen = true
|
|
if r["sealed"] != "sealed-for-"+want {
|
|
t.Errorf("%s was given %v, which belongs to something else", want, r["sealed"])
|
|
}
|
|
}
|
|
if !seen {
|
|
t.Errorf("%s resolved and its credential file was never written", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The provider is told about all three, separately, and names each grant after the module.
|
|
func TestAProviderIsToldAboutEveryConsumerOnOneMachine(t *testing.T) {
|
|
provider, err := Resolve(shelf(Manifest{
|
|
Module: "postgres", Version: "1", Provides: 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(), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var grants []Grant
|
|
for _, who := range []string{"gitea", "keycloak", "umami"} {
|
|
grants = append(grants, Grant{
|
|
Provision: "postgres-database", Consumer: "anchor", From: who,
|
|
Values: map[string]any{"name": who}, Sealed: "sealed-for-" + who})
|
|
}
|
|
out, err := provider.Declaration(Rendering{Grants: grants})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, who := range []string{"gitea", "keycloak", "umami"} {
|
|
path := "/var/lib/postgres/grants/anchor." + who + ".secret"
|
|
var found bool
|
|
for _, r := range out {
|
|
if r["path"] == path {
|
|
found = true
|
|
if r["sealed"] != "sealed-for-"+who {
|
|
t.Errorf("%s's grant holds %v", who, r["sealed"])
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("the provider was never told to create a login for %s", who)
|
|
}
|
|
}
|
|
|
|
// And all three appear in the readable manifest, so the provisioner sees three consumers
|
|
// where there are three. Named after one machine, they were one path and the last won.
|
|
for _, r := range out {
|
|
if r["path"] != "/var/lib/postgres/grants/mesh.json" {
|
|
continue
|
|
}
|
|
body := r["content"].(string)
|
|
for _, who := range []string{"gitea", "keycloak", "umami"} {
|
|
if !strings.Contains(body, `"`+who+`"`) {
|
|
t.Errorf("the provider's manifest never mentions %s: %s", who, body)
|
|
}
|
|
}
|
|
}
|
|
}
|