mesh/merge-gate head e7ff7c3045fc merged into trunk afb94d925e94 — pass: builds mesh-provisioner-client → ace, g14, novox, shanks; no bus step; every machi…
mesh/repo-check head e7ff7c3045fc merged into trunk afb94d925e94 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
263 lines
12 KiB
Go
263 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
)
|
|
|
|
// A provider's consumers' credentials, filled by its machine's client (novox/hq ADR 0373, issue 679), against a real
|
|
// bus whose accounts are the client's as mesh-controller composes them: a provider template with
|
|
// ${consumers:<provision>} is filled with one entry per bound consumer, in the plan's provisions order, each carrying
|
|
// the credential the vault holds for that pair; a consumer that binds or unbinds, or a credential the vault remakes,
|
|
// changes the file within one watch; no value crosses the bus in the clear. And a consumer's ${bound:…} is filled from
|
|
// its provider's entry (mesh-sdk go/provider, issue 678).
|
|
|
|
const consumersFile = "/var/lib/postgres/consumers.json"
|
|
|
|
// registryOf679 writes the registry's two answers this issue adds, as mesh-provisioner writes them.
|
|
func registryOf679(t *testing.T, b *aBus) {
|
|
t.Helper()
|
|
b.put(t, RegistryBucket, "consumers", `{"kind":"consumers","domain":"mesh-vault","from":"ask","ask":"mesh-vault.get"}`)
|
|
b.put(t, RegistryBucket, "bound", `{"kind":"bound","domain":"provider","from":"bucket",`+
|
|
`"bucket":"<provider-module>_bound","key":"<node>.<module>.<provision>[.<local>]"}`)
|
|
}
|
|
|
|
// bind writes another machine's module's assignment, whose plan binds it to providers, as the controller does.
|
|
func (b *aBus) bind(t *testing.T, at, module string, provisions ...PlannedProvision) {
|
|
t.Helper()
|
|
ps, err := json.Marshal(provisions)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b.put(t, AssignmentsBucket, at+"."+module, fmt.Sprintf(`{"node":%q,"module":%q,"assigned":"2026-10-14T20:00:00Z",`+
|
|
`"provisioning":{"templates":{"source":"x","digest":"sha256:%s"},"files":[{"id":"c","path":"/etc/c",`+
|
|
`"template":"c","owner-at-runtime":"owned"}],"provisions":%s}}`, at, module, strings.Repeat("0", 64), ps))
|
|
}
|
|
|
|
func postgresPlan(t *testing.T, text string) Plan {
|
|
tpl, _ := templates(t, map[string]string{"consumers.json": text})
|
|
return Plan{Templates: tpl, Files: []PlannedFile{{ID: "consumers", Path: consumersFile, Template: "consumers.json",
|
|
OwnerAtRuntime: "owned"}}}
|
|
}
|
|
|
|
func fromHere(local string) PlannedProvision {
|
|
return PlannedProvision{Provision: "postgres-database", Local: local, Provider: node, ProviderModule: "postgres"}
|
|
}
|
|
|
|
func pairOf(at, module, local string) string {
|
|
n := "pair/" + node + "/postgres-database/" + at + "/" + module
|
|
if local != "" {
|
|
n += "/" + local
|
|
}
|
|
return n
|
|
}
|
|
|
|
type listed struct {
|
|
Consumer string `json:"consumer"`
|
|
Module string `json:"module"`
|
|
Local string `json:"local,omitempty"`
|
|
Credential string `json:"credential"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
func consumersIn(t *testing.T, e *engine) []listed {
|
|
t.Helper()
|
|
var out []listed
|
|
if err := json.Unmarshal([]byte(e.read(t, consumersFile)), &out); err != nil {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
func sameList(have []listed, want ...listed) bool {
|
|
if len(have) != len(want) {
|
|
return false
|
|
}
|
|
for i := range want {
|
|
if have[i] != want[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func TestNatsAProvidersConsumersAreFilledFromTheVaultAndFollowEachBindAndRemake(t *testing.T) {
|
|
b := startBus(t)
|
|
registryOf679(t, b)
|
|
heard := b.listen(t)
|
|
wire := sniff(t, b)
|
|
vault := startVault(t, b)
|
|
vault.keep(pairOf("ace", "letta", ""), "letta-pw-never-on-the-bus", false)
|
|
vault.keep(pairOf("shanks", "gitea", "cache"), "gitea-pw-never-on-the-bus", false)
|
|
// Bound to this provider: ace's letta, and shanks's gitea under a local name. Not: a provision of another
|
|
// provider, another provision, and a credential its provider's software issued per holder (issue 757).
|
|
b.bind(t, "ace", "letta", fromHere(""),
|
|
PlannedProvision{Provision: "postgres-database", Local: "old", Provider: "ace", ProviderModule: "postgres"})
|
|
b.bind(t, "shanks", "gitea", PlannedProvision{Provision: "redis", Provider: node, ProviderModule: "postgres"},
|
|
fromHere("cache"),
|
|
PlannedProvision{Name: "token", Provision: "forge-read", Provider: node, ProviderModule: "postgres"})
|
|
// Nor a bind whose offer names another credential form than a login per consumer (issue 768).
|
|
b.bind(t, "ace", "shared", PlannedProvision{Provision: "postgres-database", Provider: node,
|
|
ProviderModule: "postgres", CredentialForm: "shared"})
|
|
e := startEngine(t, map[string]string{consumersFile: "owned"})
|
|
b.assign(t, "postgres", postgresPlan(t, "${consumers:postgres-database}"))
|
|
aClient(t, b, e, t.TempDir())
|
|
|
|
letta := listed{Consumer: "ace", Module: "letta", Credential: pairOf("ace", "letta", ""),
|
|
Value: "letta-pw-never-on-the-bus"}
|
|
gitea := listed{Consumer: "shanks", Module: "gitea", Local: "cache", Credential: pairOf("shanks", "gitea", "cache"),
|
|
Value: "gitea-pw-never-on-the-bus"}
|
|
eventually(t, "the file lists each bound consumer with its credential, in the plans' order", func() bool {
|
|
return sameList(consumersIn(t, e), letta, gitea)
|
|
})
|
|
eventually(t, "prov.applied says applied", func() bool {
|
|
r, _ := heard.last("postgres")
|
|
return r.Status == StatusApplied
|
|
})
|
|
|
|
// A consumer binds, on this machine: listed once the vault holds its credential; until then left out, and the
|
|
// others kept.
|
|
b.bind(t, node, "photos", fromHere(""))
|
|
time.Sleep(300 * time.Millisecond)
|
|
if !sameList(consumersIn(t, e), letta, gitea) {
|
|
t.Fatalf("a consumer whose credential the vault does not hold yet changed the file: %+v", consumersIn(t, e))
|
|
}
|
|
vault.keep(pairOf(node, "photos", ""), "photos-pw-never-on-the-bus", true)
|
|
photos := listed{Consumer: node, Module: "photos", Credential: pairOf(node, "photos", ""),
|
|
Value: "photos-pw-never-on-the-bus"}
|
|
eventually(t, "a consumer that binds is listed when the vault holds its credential, in its assignment's order",
|
|
func() bool { return sameList(consumersIn(t, e), letta, photos, gitea) })
|
|
// The vault remakes a credential.
|
|
vault.keep(pairOf("ace", "letta", ""), "letta-pw-remade-never-on-the-bus", true)
|
|
letta.Value = "letta-pw-remade-never-on-the-bus"
|
|
eventually(t, "a credential the vault remakes reaches the file", func() bool {
|
|
return sameList(consumersIn(t, e), letta, photos, gitea)
|
|
})
|
|
// A consumer unbinds.
|
|
b.del(t, AssignmentsBucket, "shanks.gitea")
|
|
eventually(t, "a consumer that unbinds leaves the file", func() bool {
|
|
return sameList(consumersIn(t, e), letta, photos)
|
|
})
|
|
// Another provider's consumer's credential changing is no reason to ask.
|
|
before := vault.times(pairOf("ace", "letta", ""))
|
|
vault.keep("pair/ace/postgres-database/ace/letta/old", "elsewhere", true)
|
|
time.Sleep(300 * time.Millisecond)
|
|
if vault.times("pair/ace/postgres-database/ace/letta/old") != 0 || vault.times(pairOf("ace", "letta", "")) != before {
|
|
t.Error("a change of another provider's consumer made the client ask")
|
|
}
|
|
|
|
if wire.heard() == 0 {
|
|
t.Fatal("the sniffer heard nothing: it proves nothing")
|
|
}
|
|
for _, value := range []string{"letta-pw", "gitea-pw", "photos-pw"} {
|
|
if said := wire.carrying(value); len(said) > 0 {
|
|
t.Errorf("a consumer's credential crossed the bus in the clear: %q", said)
|
|
}
|
|
}
|
|
// The need is recorded as any other, by name: never a value.
|
|
if keys := b.keys(t, NeedsBucket, node+".postgres."); len(keys) != 1 ||
|
|
keys[0] != node+".postgres.consumers.postgres-database" {
|
|
t.Errorf("the provider's needs are %v", keys)
|
|
}
|
|
}
|
|
|
|
// A provider with no consumer bound is filled with an empty list, never left waiting.
|
|
func TestNatsAProviderWithNoConsumerIsFilledWithAnEmptyList(t *testing.T) {
|
|
b := startBus(t)
|
|
registryOf679(t, b)
|
|
startVault(t, b)
|
|
e := startEngine(t, map[string]string{consumersFile: "owned"})
|
|
b.assign(t, "postgres", postgresPlan(t, "${consumers:postgres-database}"))
|
|
aClient(t, b, e, t.TempDir())
|
|
eventually(t, "the file holds an empty list", func() bool { return e.read(t, consumersFile) == "[]" })
|
|
}
|
|
|
|
// An item is kept in a bucket, where no secret is: an item naming its consumers fails.
|
|
func TestAnItemNamingConsumersIsRefused(t *testing.T) {
|
|
p := Plan{Items: []PlannedItem{{Collection: "c", Template: "item"}}}
|
|
if _, why := itemTargets(p, map[string]string{"item": "${consumers:postgres-database}"}); !strings.Contains(why,
|
|
"no secret") {
|
|
t.Fatalf("an item naming consumers: %q", why)
|
|
}
|
|
}
|
|
|
|
const appBound = "/etc/app/database.env"
|
|
|
|
func appPlan(t *testing.T) Plan {
|
|
tpl, _ := templates(t, map[string]string{"database.env": "HOST=${bound:postgres-database:host}\n" +
|
|
"PORT=${bound:postgres-database:port}\nUSER=${bound:postgres-database:user}\n"})
|
|
return Plan{Templates: tpl, Files: []PlannedFile{{ID: "db", Path: appBound, Template: "database.env",
|
|
OwnerAtRuntime: "owned"}},
|
|
Provisions: []PlannedProvision{{Provision: "postgres-database", Provider: "novox", ProviderModule: "postgres"}}}
|
|
}
|
|
|
|
func TestNatsAConsumersBoundValuesAreFilledFromItsProvidersEntry(t *testing.T) {
|
|
b := startBus(t)
|
|
registryOf679(t, b)
|
|
heard := b.listen(t)
|
|
e := startEngine(t, map[string]string{appBound: "owned"})
|
|
b.assign(t, "app", appPlan(t))
|
|
aClient(t, b, e, t.TempDir())
|
|
|
|
// No bucket of the provider's yet: waited for, and said.
|
|
eventually(t, "the consumer waits for its provider's bucket", func() bool {
|
|
r, _ := heard.last("app")
|
|
return r.Status == StatusWaiting && strings.Contains(strings.Join(r.WaitsFor, " "), "postgres_bound")
|
|
})
|
|
kv, err := b.domain.CreateKeyValue(context.Background(), jetstream.KeyValueConfig{Bucket: "postgres_bound"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b.kv["postgres_bound"] = kv
|
|
// The provider has not answered it yet: waited for.
|
|
b.put(t, "postgres_bound", "ace.app.postgres-database", `{"values":{"host":"elsewhere"}}`)
|
|
b.assign(t, "app", appPlan(t)) // a change of its own is the only reason it looks again: there is no timer
|
|
eventually(t, "the consumer waits for its own entry", func() bool {
|
|
r, _ := heard.last("app")
|
|
return r.Status == StatusWaiting && strings.Contains(strings.Join(r.WaitsFor, " "), "postgres on novox")
|
|
})
|
|
b.put(t, "postgres_bound", node+".app.postgres-database", `{"provider":"novox","provider-module":"postgres",`+
|
|
`"provision":"postgres-database","consumer":"g14","module":"app","as":"mesh_g14_app","user":"mesh_g14_app",`+
|
|
`"values":{"host":"novox.mesh","port":5432},"vault-name":"pair/novox/postgres-database/g14/app"}`)
|
|
eventually(t, "the file is filled from its provider's entry", func() bool {
|
|
return e.read(t, appBound) == "HOST=novox.mesh\nPORT=5432\nUSER=mesh_g14_app\n"
|
|
})
|
|
b.put(t, "postgres_bound", node+".app.postgres-database", `{"user":"mesh_g14_app",`+
|
|
`"values":{"host":"novox.lan","port":5432}}`)
|
|
eventually(t, "a changed entry refills the file", func() bool {
|
|
return e.read(t, appBound) == "HOST=novox.lan\nPORT=5432\nUSER=mesh_g14_app\n"
|
|
})
|
|
b.put(t, "postgres_bound", node+".app.postgres-database", `{"refused":"app on g14 keeps several holders"}`)
|
|
eventually(t, "an entry the provider refused fails the consumer with its reason", func() bool {
|
|
r, _ := heard.last("app")
|
|
return r.Status == StatusFailed && strings.Contains(r.Reason, "keeps several holders")
|
|
})
|
|
}
|
|
|
|
// A consumer whose plan names its login's file (issue 779) is still a bind of its provider: it is listed, where an
|
|
// entry naming a value its provider's software issued per holder (issue 757) is not.
|
|
func TestNatsAProviderListsAConsumerWhoseEntryNamesItsLoginFile(t *testing.T) {
|
|
b := startBus(t)
|
|
registryOf679(t, b)
|
|
vault := startVault(t, b)
|
|
vault.keep(pairOf("ace", "nextcloud", ""), "nc-pw-never-on-the-bus", false)
|
|
vault.keep(pairOf("ace", "forge", ""), "forge-pw", false)
|
|
b.bind(t, "ace", "nextcloud", PlannedProvision{Name: "database", Provision: "postgres-database", Provider: node,
|
|
ProviderModule: "postgres", CredentialForm: "pair", Path: "/s/database.secret"})
|
|
b.bind(t, "ace", "forge", PlannedProvision{Name: "database", Provision: "postgres-database", Provider: node,
|
|
ProviderModule: "postgres", CredentialForm: "issued", Path: "/s/forge.secret"})
|
|
e := startEngine(t, map[string]string{consumersFile: "owned"})
|
|
b.assign(t, "postgres", postgresPlan(t, "${consumers:postgres-database}"))
|
|
aClient(t, b, e, t.TempDir())
|
|
eventually(t, "the consumer with a login file is listed, the per-holder one is not", func() bool {
|
|
return sameList(consumersIn(t, e), listed{Consumer: "ace", Module: "nextcloud",
|
|
Credential: pairOf("ace", "nextcloud", ""), Value: "nc-pw-never-on-the-bus"})
|
|
})
|
|
}
|