Two things, both found by trying to write a real postgres module and discovering it could not be said. A database has a superuser password, a broker an administrator, a registry an account. None of them is *for* anybody — they are not the credential a consumer is given, and the mechanism that hands those out has a consumer in the middle of it. So a module may declare what it needs and where to put it, and the mesh generates one per node, seals it, and reads it no more than it reads any other. Per node, deliberately: a module running on three machines has three passwords. One in the manifest instead would put the same secret on every machine that ever runs it, in a file anybody can read, for ever. Made once and kept, or a running database would be handed a password it was not started with; remade when the machine's sealing key changes, like everything else sealed here. A need declared and not made is refused rather than skipped, because a module whose own credential is silently absent starts, fails to authenticate, and the reason is three layers from the machine reporting it. And the provisioner can watch. That is what lets it be a module rather than a binary somebody places: run once, it needs invoking after every declaration by a timer or a unit wired to a file; watching, it is an ordinary long-running service the host already supervises. It polls rather than watching the filesystem, because the host writes atomically — the file is replaced, so a watch on the path stops seeing anything after the first replacement, and a watcher that silently stops working is worse than a poll. Credentials are compared by digest and never held: this runs for as long as the machine is up.
95 lines
2.9 KiB
Go
95 lines
2.9 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A secret a module needs in order to be itself.
|
|
//
|
|
// A database has a superuser password, a broker an administrator, a registry an account. None is
|
|
// *for* anybody — it is not the credential a consumer is given, and the mechanism that hands
|
|
// those out has a consumer in the middle of it.
|
|
|
|
func needy() Manifest {
|
|
return Manifest{
|
|
Module: "postgres", Version: "1",
|
|
Needs: map[string]string{"superuser": "/var/lib/mesh/postgres/superuser"},
|
|
Resources: []map[string]any{
|
|
{"id": "store", "type": "container", "name": "mesh-postgres", "image": "postgres@sha256:x"},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestAModulesOwnSecretLandsSealed(t *testing.T) {
|
|
got, err := Resolve(shelf(needy()), []string{"postgres"}, reachable(), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := got.Declaration(Rendering{
|
|
Needed: map[string]map[string]string{"postgres": {"superuser": "c2VhbGVk"}},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, r := range out {
|
|
if r["path"] != "/var/lib/mesh/postgres/superuser" {
|
|
continue
|
|
}
|
|
if r["sealed"] != "c2VhbGVk" {
|
|
t.Fatalf("got %v", r)
|
|
}
|
|
if r["content"] != nil {
|
|
t.Fatal("a module's own secret was written in the clear")
|
|
}
|
|
return
|
|
}
|
|
t.Fatalf("no secret was written: %v", out)
|
|
}
|
|
|
|
func TestADeclaredNeedThatWasNotMadeIsRefused(t *testing.T) {
|
|
// Skipping it would start a database with no password it knows, which fails to authenticate
|
|
// three layers from the machine reporting it.
|
|
got, _ := Resolve(shelf(needy()), []string{"postgres"}, reachable(), World{})
|
|
_, err := got.Declaration(Rendering{})
|
|
if err == nil {
|
|
t.Fatal("a module needing a secret was declared without one")
|
|
}
|
|
if !strings.Contains(err.Error(), "superuser") {
|
|
t.Fatalf("the refusal does not name what is missing: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestANeedIsAnAbsolutePath(t *testing.T) {
|
|
_, err := ParseManifest([]byte(`{"module":"postgres","version":"1",
|
|
"needs":{"superuser":"superuser.txt"}}`))
|
|
if err == nil {
|
|
t.Fatal("a relative path was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), "absolute path") {
|
|
t.Fatalf("unhelpful refusal: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAModuleMayNeedSeveralThings(t *testing.T) {
|
|
// A password and a token, say. Telling them apart is the module's business, not the mesh's.
|
|
m := needy()
|
|
m.Needs["replication"] = "/var/lib/mesh/postgres/replication"
|
|
got, _ := Resolve(shelf(m), []string{"postgres"}, reachable(), World{})
|
|
out, err := got.Declaration(Rendering{Needed: map[string]map[string]string{
|
|
"postgres": {"superuser": "b25l", "replication": "dHdv"},
|
|
}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seen := map[string]string{}
|
|
for _, r := range out {
|
|
if path, ok := r["path"].(string); ok && strings.Contains(path, "/var/lib/mesh/postgres/") {
|
|
seen[path], _ = r["sealed"].(string)
|
|
}
|
|
}
|
|
if len(seen) != 2 || seen["/var/lib/mesh/postgres/superuser"] == seen["/var/lib/mesh/postgres/replication"] {
|
|
t.Fatalf("two needs did not land as two secrets: %v", seen)
|
|
}
|
|
}
|