Files
mesh-catalog/modules/mesh-provisioner/cmd/mesh-provisioner/provisioner_test.go
T

307 lines
12 KiB
Go

package main
import (
"encoding/json"
"os"
"slices"
"sort"
"strings"
"sync"
"testing"
)
// memStore is the bus as mesh-provisioner reaches it, in memory: the states it keeps.
type memStore struct {
mu sync.Mutex
states map[string]map[string][]byte
puts int
}
func newStore() *memStore { return &memStore{states: map[string]map[string][]byte{}} }
func (m *memStore) Get(state, key string) (*Entry, error) {
m.mu.Lock()
defer m.mu.Unlock()
v, ok := m.states[state][key]
if !ok {
return nil, nil
}
return &Entry{Key: key, Value: append([]byte(nil), v...)}, nil
}
func (m *memStore) Put(state, key string, value any) error {
raw, err := json.Marshal(value)
if err != nil {
return err
}
m.mu.Lock()
defer m.mu.Unlock()
if m.states[state] == nil {
m.states[state] = map[string][]byte{}
}
m.states[state][key] = raw
m.puts++
return nil
}
func (m *memStore) Delete(state, key string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.states[state], key)
return nil
}
func (m *memStore) Keys(state string) ([]string, error) {
m.mu.Lock()
defer m.mu.Unlock()
var keys []string
for k := range m.states[state] {
keys = append(keys, k)
}
sort.Strings(keys)
return keys, nil
}
// write is a client's write, as the bus lets it through for its own machine's keys.
func (m *memStore) write(t *testing.T, state, key, raw string) {
t.Helper()
if err := m.Put(state, key, json.RawMessage(raw)); err != nil {
t.Fatal(err)
}
}
// The registry answers every kind a moved module's template may name (ADR 0349 clause 2): config, secret, bound,
// collected and the placement namespaces, each with its domain and where a client finds it.
func TestTheRegistryAnswersEveryKindATemplateMayName(t *testing.T) {
var kinds []string
for _, a := range TheRegistry {
kinds = append(kinds, a.Kind)
if a.Domain == "" || a.Why == "" || !strings.HasPrefix(a.Decision, "novox/hq ADR ") {
t.Errorf("%s: an answer names its domain, why and its decision: %+v", a.Kind, a)
}
switch a.From {
case FromBucket:
if a.Bucket == "" || !strings.HasPrefix(a.Key, "<node>.") {
t.Errorf("%s: a value in a bucket names the bucket and a key with the node first: %+v", a.Kind, a)
}
if a.Ask != "" {
t.Errorf("%s: from a bucket, asked of nobody: %+v", a.Kind, a)
}
case FromAsk:
if a.Ask == "" || a.Bucket != "" {
t.Errorf("%s: a value asked for names its verb and no bucket: %+v", a.Kind, a)
}
default:
t.Errorf("%s: from %q", a.Kind, a.From)
}
}
want := []string{"config", "secret", "bound", "consumers", "collected", "shell", "environment", "contribution",
"dir", "access", "port", "seat", "machine", "backup"}
if !slices.Equal(kinds, want) {
t.Errorf("the registry's kinds are %v, want %v", kinds, want)
}
// The buckets a client is granted to read its node's keys of (mesh-controller internal/broker/provisioner.go):
// the config domain's resolved values, this module's own collections, and placement in each module's assignment,
// where the controller carries the provisioner's plan (issue 671): a client never reads the node's declaration.
for kind, bucket := range map[string]string{"config": "mesh-config_resolved", "collected": "mesh-provisioner_" + Collect,
"shell": "mesh-provisioner_" + Collect, "environment": "mesh-provisioner_" + Collect,
"contribution": "mesh-provisioner_" + Collect,
"dir": "mesh-controller_assignments", "access": "mesh-controller_assignments",
"port": "mesh-controller_assignments", "seat": "mesh-controller_assignments",
"machine": "mesh-controller_assignments", "backup": "mesh-controller_assignments"} {
i := slices.IndexFunc(TheRegistry, func(a Answer) bool { return a.Kind == kind })
if TheRegistry[i].Bucket != bucket {
t.Errorf("%s is found in %s, want %s", kind, TheRegistry[i].Bucket, bucket)
}
}
// A secret is never in a bucket (ADR 0201 §13): asked of the vault.
i := slices.IndexFunc(TheRegistry, func(a Answer) bool { return a.Kind == "secret" })
if a := TheRegistry[i]; a.From != FromAsk || a.Ask != "mesh-vault.get" {
t.Errorf("a secret is answered by %+v, want asked of mesh-vault.get", a)
}
// A consumer's values are in its provider's own bucket, one entry per consumer, keyed as the providers' helper
// writes it (mesh-sdk go/provider, issue 678): the consumer's machine first.
i = slices.IndexFunc(TheRegistry, func(a Answer) bool { return a.Kind == "bound" })
if a := TheRegistry[i]; a.From != FromBucket || a.Bucket != "<provider-module>_bound" ||
a.Key != "<node>.<module>.<provision>[.<local>]" {
t.Errorf("a bound value is answered by %+v, want the provider's <provider-module>_bound, "+
"<node>.<module>.<provision>[.<local>]", a)
}
// A provider's consumers' credentials are the vault's, asked by the provider machine's client (ADR 0373).
i = slices.IndexFunc(TheRegistry, func(a Answer) bool { return a.Kind == "consumers" })
if a := TheRegistry[i]; a.Domain != "mesh-vault" || a.From != FromAsk || a.Ask != "mesh-vault.get" ||
a.Decision != "novox/hq ADR 0373" {
t.Errorf("a provider's consumers are answered by %+v, want the vault, asked of mesh-vault.get (ADR 0373)", a)
}
}
// The registry is written whole at start, a pass that finds it equal writes nothing, a changed answer is written
// back, and a kind no domain answers any more is deleted.
func TestTheRegistryIsWrittenBackAndAQuietPassWritesNothing(t *testing.T) {
s := newStore()
written, deleted, err := Publish(s)
if err != nil {
t.Fatal(err)
}
if len(written) != len(TheRegistry) || len(deleted) != 0 {
t.Fatalf("the first pass wrote %v and deleted %v", written, deleted)
}
for _, a := range TheRegistry {
e, _ := s.Get(Registry, a.Kind)
var got Answer
if e == nil || json.Unmarshal(e.Value, &got) != nil || got != a {
t.Errorf("the registry's %s holds %v, want %+v", a.Kind, e, a)
}
}
puts := s.puts
if written, deleted, err := Publish(s); err != nil || len(written)+len(deleted) != 0 || s.puts != puts {
t.Fatalf("a quiet pass wrote %v, deleted %v (%v)", written, deleted, err)
}
s.write(t, Registry, "config", `{"kind":"config","domain":"mesh-controller"}`)
s.write(t, Registry, "setting", `{"kind":"setting"}`)
written, deleted, err = Publish(s)
if err != nil || !slices.Equal(written, []string{"config"}) || !slices.Equal(deleted, []string{"setting"}) {
t.Fatalf("a changed and an unknown kind: wrote %v, deleted %v (%v)", written, deleted, err)
}
}
func clientWrites(t *testing.T) *memStore {
s := newStore()
if _, _, err := Publish(s); err != nil {
t.Fatal(err)
}
s.write(t, Needs, "g14.dunst.config.font-size", `{"at":"2026-10-14T20:00:00Z"}`)
s.write(t, Needs, "g14.dunst.secret.token", `{}`)
s.write(t, Needs, "shanks.dunst.config.font-size", `{}`)
s.write(t, Needs, "shanks.gitea.bound.database.user", `{}`)
s.write(t, Applied, "g14.dunst", `{"build":"b1","outcome":"applied"}`)
s.write(t, Applied, "shanks.dunst", `{"build":"b1","outcome":"failed","reason":"waits for config font-size"}`)
s.write(t, Collect, "g14.zsh-snippets.git", `{}`)
s.write(t, Collect, "g14.bar-blocks.disk-load", `{}`)
s.write(t, Collect, "g14.shell=3Azsh=3Anormal.mesh-cli", `{}`)
return s
}
func keysOf(r Read) []string {
var out []string
for _, row := range r.Rows {
out = append(out, row.Key)
}
return out
}
// Each bucket is read by its shape, its parts named, and narrowed by node, module and collection.
func TestEachBucketIsReadByItsShapeAndNarrowed(t *testing.T) {
s := clientWrites(t)
for _, c := range []struct {
q Query
want []string
}{
{Query{}, []string{"access", "backup", "bound", "collected", "config", "consumers", "contribution", "dir", "environment",
"machine", "port", "seat", "secret", "shell"}},
{Query{What: Needs}, []string{"g14.dunst.config.font-size", "g14.dunst.secret.token",
"shanks.dunst.config.font-size", "shanks.gitea.bound.database.user"}},
{Query{What: Needs, Node: "g14"}, []string{"g14.dunst.config.font-size", "g14.dunst.secret.token"}},
{Query{What: Needs, Module: "dunst", Node: "shanks"}, []string{"shanks.dunst.config.font-size"}},
{Query{What: Applied, Module: "dunst"}, []string{"g14.dunst", "shanks.dunst"}},
{Query{What: Applied, Node: "ace"}, nil},
{Query{What: Collect, Collection: "zsh-snippets"}, []string{"g14.zsh-snippets.git"}},
{Query{What: Collect, Module: "disk-load"}, []string{"g14.bar-blocks.disk-load"}},
// An old namespace's collection, by its name, not its key's token (design 57 L1).
{Query{What: Collect, Collection: "shell:zsh:normal"}, []string{"g14.shell=3Azsh=3Anormal.mesh-cli"}},
} {
r, err := Do(s, c.q)
if err != nil {
t.Fatalf("%+v: %v", c.q, err)
}
if got := keysOf(r); !slices.Equal(got, c.want) || r.Count != len(c.want) {
t.Errorf("%+v: read %v (%d), want %v", c.q, got, r.Count, c.want)
}
}
r, _ := Do(s, Query{What: Needs, Node: "shanks", Module: "gitea"})
if row := r.Rows[0]; row.Node != "shanks" || row.Module != "gitea" || row.Kind != "bound" || row.Name != "database.user" {
t.Errorf("a need's parts: %+v", row)
}
r, _ = Do(s, Query{What: Collect, Collection: "bar-blocks"})
if row := r.Rows[0]; row.Node != "g14" || row.Collection != "bar-blocks" || row.Module != "disk-load" {
t.Errorf("an item's parts: %+v", row)
}
r, _ = Do(s, Query{What: Applied, Node: "shanks"})
if string(r.Rows[0].Value) != `{"build":"b1","outcome":"failed","reason":"waits for config font-size"}` {
t.Errorf("what is applied is read as the client wrote it: %s", r.Rows[0].Value)
}
}
// A key out of its bucket's shape is said, never dropped quietly.
func TestAKeyOutOfItsBucketsShapeIsSaid(t *testing.T) {
s := clientWrites(t)
s.write(t, Applied, "g14.dunst.extra", `{}`)
r, err := Do(s, Query{What: Applied})
if err != nil {
t.Fatal(err)
}
if r.Count != 2 || !strings.Contains(r.Said, "g14.dunst.extra") || !strings.Contains(r.Said, "<node>.<module>") {
t.Errorf("an unshaped key: %d rows, said %q", r.Count, r.Said)
}
}
// The verb's arguments are read strictly, and a narrowing that means nothing is refused rather than ignored.
func TestTheVerbsArgumentsAreReadStrictly(t *testing.T) {
s := clientWrites(t)
run := Tools(s)[0].Run
if Tools(s)[0].Name != "mesh-provisioner.provisioning" {
t.Fatalf("the verb is served as %s", Tools(s)[0].Name)
}
for _, args := range []map[string]any{
{"what": "needs", "caller": "g14.dunst"},
{"node": 3},
{"what": "secrets"},
{"node": "g14"},
{"what": "needs", "collection": "zsh-snippets"},
} {
if _, err := run(args); err == nil {
t.Errorf("%v was taken", args)
}
}
got, err := run(map[string]any{"what": "applied", "node": " g14 "})
if err != nil {
t.Fatal(err)
}
if r := got.(Read); r.Count != 1 || r.Said != "1 in applied for node g14" {
t.Errorf("a read: %+v", r)
}
}
// The manifest declares the seat, its verb, the four buckets this code keeps, and an account of its own, so that
// its account alone (with each client for its machine's keys) writes them.
func TestTheManifestDeclaresWhatTheCodeKeeps(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m struct {
Module string `json:"module"`
RunsAs string `json:"runs-as"`
Claims []struct {
Name string `json:"name"`
Scope string `json:"scope"`
Serves []string `json:"serves"`
} `json:"claims"`
State []string `json:"state"`
OwnSecrets map[string]string `json:"own-secrets"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
if m.Module != "mesh-provisioner" || m.RunsAs != "mesh-provisioner" || m.OwnSecrets["broker"] == "" {
t.Errorf("an account of its own: module %q, runs-as %q, own secrets %v", m.Module, m.RunsAs, m.OwnSecrets)
}
if len(m.Claims) != 1 || m.Claims[0].Name != Seat || m.Claims[0].Scope != "mesh" ||
!slices.Equal(m.Claims[0].Serves, []string{Verb}) {
t.Errorf("the claim: %+v", m.Claims)
}
if want := []string{Registry, Needs, Applied, Collect}; !slices.Equal(m.State, want) {
t.Errorf("the state is %v, want %v", m.State, want)
}
}