A provider failed every consumer for a day and said so only in its journal (hq issue 179). The provisioner loop now emits provisioner.failing after five minutes without a success — create, check or secret — and repeats it every fifteen; provisioner.recovered on the next success, on withdrawal, and on the first success after a restart, so the controller can name it in status (hq ADR 0224).
239 lines
6.5 KiB
Go
239 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type recorder struct {
|
|
created []Provision
|
|
removed []string
|
|
held bool
|
|
failing error
|
|
holdsErr error
|
|
}
|
|
|
|
func (r *recorder) Create(_ context.Context, p Provision) error {
|
|
if r.failing != nil {
|
|
return r.failing
|
|
}
|
|
r.created = append(r.created, p)
|
|
return nil
|
|
}
|
|
|
|
func (r *recorder) Remove(_ context.Context, as string, _ map[string]any) error {
|
|
r.removed = append(r.removed, as)
|
|
return nil
|
|
}
|
|
|
|
func (r *recorder) Holds(context.Context, Provision) (bool, error) {
|
|
if r.holdsErr != nil {
|
|
return false, r.holdsErr
|
|
}
|
|
return r.held, nil
|
|
}
|
|
|
|
type world struct {
|
|
t *testing.T
|
|
dir string
|
|
receives string
|
|
now time.Time
|
|
h *Harness
|
|
a *recorder
|
|
said []string
|
|
}
|
|
|
|
func newWorld(t *testing.T) *world {
|
|
w := &world{t: t, dir: t.TempDir(), now: time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC), a: &recorder{held: true}}
|
|
w.receives = filepath.Join(w.dir, "mesh.json")
|
|
w.h = &Harness{Resource: "postgres-database", Receives: w.receives, Adapter: w.a,
|
|
Now: func() time.Time { return w.now },
|
|
Log: func(f string, a ...any) { w.said = append(w.said, f) }}
|
|
return w
|
|
}
|
|
|
|
func (w *world) give(given ...map[string]any) {
|
|
for _, g := range given {
|
|
secret := filepath.Join(w.dir, g["as"].(string)+".secret")
|
|
if err := os.WriteFile(secret, []byte("pw-"+g["as"].(string)+"\n"), 0o600); err != nil {
|
|
w.t.Fatal(err)
|
|
}
|
|
g["secret"] = secret
|
|
}
|
|
if given == nil {
|
|
given = []map[string]any{}
|
|
}
|
|
raw, _ := json.Marshal(map[string]any{"requirement": "postgres-database", "given": given})
|
|
if err := os.WriteFile(w.receives, raw, 0o600); err != nil {
|
|
w.t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestAConsumerIsCreatedOnceUnderTheMeshsLoginAndPassword(t *testing.T) {
|
|
w := newWorld(t)
|
|
w.give(map[string]any{"as": "mesh_ace_letta", "node": "ace", "values": map[string]any{"name": "letta"}})
|
|
w.h.Reconcile(ctx)
|
|
w.h.Reconcile(ctx)
|
|
if len(w.a.created) != 1 {
|
|
t.Fatalf("created %d times", len(w.a.created))
|
|
}
|
|
p := w.a.created[0]
|
|
if p.As != "mesh_ace_letta" || p.Password != "pw-mesh_ace_letta" || p.Consumer != "ace" {
|
|
t.Fatalf("%+v", p)
|
|
}
|
|
}
|
|
|
|
func TestAddingExtensionsAppliesTheConsumerAgain(t *testing.T) {
|
|
w := newWorld(t)
|
|
w.give(map[string]any{"as": "mesh_ace_letta", "values": map[string]any{"name": "letta"}})
|
|
w.h.Reconcile(ctx)
|
|
w.give(map[string]any{"as": "mesh_ace_letta", "values": map[string]any{"name": "letta", "extensions": []any{"vector"}}})
|
|
w.h.Reconcile(ctx)
|
|
if len(w.a.created) != 2 {
|
|
t.Fatalf("created %d times", len(w.a.created))
|
|
}
|
|
if ext, _ := Extensions(w.a.created[1].Values); len(ext) != 1 || ext[0] != "vector" {
|
|
t.Fatal(ext)
|
|
}
|
|
}
|
|
|
|
func TestAConsumerNoLongerAskedForIsWithdrawn(t *testing.T) {
|
|
w := newWorld(t)
|
|
w.give(map[string]any{"as": "a"}, map[string]any{"as": "b"})
|
|
w.h.Reconcile(ctx)
|
|
w.give(map[string]any{"as": "a"})
|
|
w.h.Reconcile(ctx)
|
|
if strings.Join(w.a.removed, ",") != "b" {
|
|
t.Fatal(w.a.removed)
|
|
}
|
|
// Only a file that says nobody asks withdraws everybody.
|
|
w.give()
|
|
w.h.Reconcile(ctx)
|
|
if strings.Join(w.a.removed, ",") != "b,a" {
|
|
t.Fatal(w.a.removed)
|
|
}
|
|
}
|
|
|
|
func TestNothingReadIsNotNobodyAsking(t *testing.T) {
|
|
for name, content := range map[string]string{
|
|
"unreadable": "",
|
|
"not JSON": "{",
|
|
"no given": `{"requirement": "postgres-database"}`,
|
|
"another": `{"requirement": "mssql-database", "given": []}`,
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
w := newWorld(t)
|
|
w.give(map[string]any{"as": "a"})
|
|
w.h.Reconcile(ctx)
|
|
if content == "" {
|
|
os.Remove(w.receives)
|
|
} else {
|
|
os.WriteFile(w.receives, []byte(content), 0o600)
|
|
}
|
|
w.h.Reconcile(ctx)
|
|
if len(w.a.removed) != 0 {
|
|
t.Fatalf("withdrew %v on a file it could not use", w.a.removed)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestALostConsumerIsAppliedAgainAndBraked(t *testing.T) {
|
|
w := newWorld(t)
|
|
w.give(map[string]any{"as": "a"})
|
|
w.h.Reconcile(ctx)
|
|
w.a.held = false
|
|
w.now = w.now.Add(2 * time.Minute)
|
|
w.h.Reconcile(ctx)
|
|
if len(w.a.created) != 2 {
|
|
t.Fatalf("created %d times", len(w.a.created))
|
|
}
|
|
// Still not held a minute later: applied again, then braked for two minutes.
|
|
w.now = w.now.Add(61 * time.Second)
|
|
w.h.Reconcile(ctx)
|
|
w.now = w.now.Add(61 * time.Second)
|
|
w.h.Reconcile(ctx)
|
|
if len(w.a.created) != 3 {
|
|
t.Fatalf("not braked: created %d times", len(w.a.created))
|
|
}
|
|
}
|
|
|
|
func TestAFailingCreateIsRetriedAndSaidOnce(t *testing.T) {
|
|
w := newWorld(t)
|
|
w.a.failing = &pgErr{"extension \"nope\" refused, password pw-a"}
|
|
w.give(map[string]any{"as": "a"})
|
|
for i := 0; i < 5; i++ {
|
|
w.h.Reconcile(ctx)
|
|
}
|
|
n := 0
|
|
for _, s := range w.said {
|
|
if strings.Contains(s, "create failed") {
|
|
n++
|
|
}
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("said %d times", n)
|
|
}
|
|
w.a.failing = nil
|
|
w.h.Reconcile(ctx)
|
|
if len(w.a.created) != 1 {
|
|
t.Fatal("not retried")
|
|
}
|
|
}
|
|
|
|
type pgErr struct{ s string }
|
|
|
|
func (e *pgErr) Error() string { return e.s }
|
|
|
|
func TestScrubRemovesThePassword(t *testing.T) {
|
|
if got := scrub(&pgErr{"bad pw a/b c and a%2Fb+c"}, "a/b c"); strings.Contains(got, "a/b c") || strings.Contains(got, "a%2Fb+c") {
|
|
t.Fatal(got)
|
|
}
|
|
}
|
|
|
|
func TestProvisionerCreatesTheDatabaseThenItsExtensions(t *testing.T) {
|
|
f, c := newFake(admin)
|
|
f.answer(`FROM pg_available_extensions`, []string{"name"}, []string{"vector"})
|
|
var events []string
|
|
a := provisioner{pg: c, announce: func(e string, _ map[string]string) { events = append(events, e) }}
|
|
p := Provision{As: "mesh_ace_letta", Password: "pw", Values: map[string]any{"name": "letta", "extensions": []any{"vector"}}}
|
|
if err := a.Create(ctx, p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sql := f.statements()
|
|
if !strings.HasPrefix(sql[len(sql)-1], `CREATE EXTENSION IF NOT EXISTS "vector"`) || !has(sql, `CREATE DATABASE "mesh_ace_letta"`) {
|
|
t.Fatal(strings.Join(sql, "\n"))
|
|
}
|
|
if strings.Join(events, ",") != "database.provisioned" {
|
|
t.Fatal(events)
|
|
}
|
|
|
|
// An extension the server does not offer: refused, and no event says it was provisioned.
|
|
events = nil
|
|
p.Values = map[string]any{"extensions": []any{"nope"}}
|
|
if err := a.Create(ctx, p); err == nil || !strings.Contains(err.Error(), `"nope"`) || len(events) != 0 {
|
|
t.Fatal(err, events)
|
|
}
|
|
// A malformed list: refused before the server is asked anything.
|
|
f.reset()
|
|
p.Values = map[string]any{"extensions": "vector"}
|
|
if err := a.Create(ctx, p); err == nil || len(f.calls) != 0 {
|
|
t.Fatal(err, f.calls)
|
|
}
|
|
|
|
f.reset()
|
|
f.answer(`FROM pg_roles`, []string{"?column?"}, []string{"1"})
|
|
if err := a.Remove(ctx, "mesh_ace_letta", nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
noDrop(t, f.statements())
|
|
if !has(f.statements(), `NOLOGIN`) {
|
|
t.Fatal(f.statements())
|
|
}
|
|
}
|