From ba2806b985ce59ad2bc3a410c0f98e0c2cdce44e Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 13 Oct 2026 10:47:34 +0200 Subject: [PATCH] =?UTF-8?q?Measure=20that=20the=20postgres=20module's=20im?= =?UTF-8?q?age=20initialises=20where=20genesis=20now=20raises=20the=20stor?= =?UTF-8?q?e=20(hq=20issue=20609,=20ADR=200338=20=C2=A75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The genesis bundle raises the store as the postgres module's server: its pgvector image, its data in a directory made before the store starts and owned as the module declares it, its password from a file. The decision names one measurement before it is relied on: that image, on such a directory, empty, initialises a store that answers with that password. The image and the owner are read from the catalogue's manifest, so the measurement moves with the module's pin. --- replays/docker.go | 3 +- replays/store_test.go | 133 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 replays/store_test.go diff --git a/replays/docker.go b/replays/docker.go index 5689294..a920f19 100644 --- a/replays/docker.go +++ b/replays/docker.go @@ -109,6 +109,7 @@ func (d *Docker) RemoveNetwork(ctx context.Context, id string) { type Run struct { Image string Cmd []string + Env []string Binds []string Network string DNS []string @@ -138,7 +139,7 @@ func (d *Docker) Start(ctx context.Context, r Run) (string, string, error) { path += "?name=" + url.QueryEscape(r.Name) } if err := d.call(ctx, http.MethodPost, path, map[string]any{"Image": r.Image, "Cmd": r.Cmd, - "Labels": labels, "HostConfig": host}, &made); err != nil { + "Env": r.Env, "Labels": labels, "HostConfig": host}, &made); err != nil { return "", "", err } if err := d.call(ctx, http.MethodPost, "/containers/"+made.ID+"/start", nil, nil); err != nil { diff --git a/replays/store_test.go b/replays/store_test.go new file mode 100644 index 0000000..5c8eb2b --- /dev/null +++ b/replays/store_test.go @@ -0,0 +1,133 @@ +package replays + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +// THE STORE GENESIS RAISES INITIALISES WHERE THE POSTGRES MODULE KEEPS IT (novox/hq ADR 0338 ยง5, issue 609). +// +// Genesis raises the store as the catalogue's postgres module declares its server: the module's image, its +// data in a directory made before the store starts and owned 999:70 as the module declares it, and its +// superuser password read from a file. The measurement the decision names: the module's image, started on +// such a directory, empty, initialises a store that answers with that password. The image is read from the +// catalogue's manifest, so the measurement moves with the module's pin. +// +// Needs a container runtime (its socket) and the catalogue (MESH_REPLAY_CATALOGUE, or the checkout beside +// the lab); skipped aloud without either. +func TestTheStoreGenesisRaisesInitialisesOnTheDirectoryGenesisMakes(t *testing.T) { + catalogue := os.Getenv("MESH_REPLAY_CATALOGUE") + if catalogue == "" { + catalogue = filepath.Join("..", "..", "mesh-catalog") + } + raw, err := os.ReadFile(filepath.Join(catalogue, "modules", "postgres", "module.json")) + if err != nil { + t.Skipf("no catalogue to read the store's image from (%v); MESH_REPLAY_CATALOGUE names one", err) + } + docker, ok := DockerFromEnv() + if !ok { + t.Skip("no container runtime: the store runs in a container") + } + var m struct { + Resources []struct { + ID, Type, Path, Owner, Mode, Image string + Env map[string]string + } `json:"resources"` + } + if err := json.Unmarshal(raw, &m); err != nil { + t.Fatal(err) + } + var image, pgdata, owner, mode string + for _, r := range m.Resources { + switch { + case r.Type == "container" && r.ID == "server": + image, pgdata = r.Image, r.Env["PGDATA"] + case r.Type == "directory" && r.ID == "store-data": + owner, mode = r.Owner, r.Mode + } + } + if image == "" || pgdata == "" || owner == "" { + t.Fatalf("the postgres module names no server image (%q), PGDATA (%q) or data directory owner (%q)", image, pgdata, owner) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + // Under the working directory: a check's workspace is mounted into its containers at the same path, + // so the runtime binds what this test made. + here, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + dir, err := os.MkdirTemp(here, "store-measure-") + if err != nil { + t.Fatal(err) + } + for _, img := range []string{"alpine:3.20", image} { + if err := docker.Pull(ctx, img); err != nil { + t.Fatal(err) + } + } + // The directory as genesis makes it, and the password file as genesis writes it: as root, a helper + // container doing what the node-engine does on the machine. + prep, _, err := docker.Start(ctx, Run{Image: "alpine:3.20", Binds: []string{dir + ":/w"}, Cmd: []string{"sh", "-c", + "mkdir -p /w/store /w/state && chown " + owner + " /w/store && chmod " + orMode(mode) + " /w/store && " + + "printf measured-pw > /w/state/superuser.secret && chmod 0600 /w/state/superuser.secret"}}) + if err != nil { + t.Fatal(err) + } + if code, said, err := docker.Wait(ctx, prep); err != nil || code != 0 { + t.Fatalf("the directory could not be made as genesis makes it: %v %d %s", err, code, said) + } + docker.Remove(ctx, prep) + t.Cleanup(func() { + clean, _, err := docker.Start(context.Background(), Run{Image: "alpine:3.20", Binds: []string{dir + ":/w"}, + Cmd: []string{"sh", "-c", "rm -rf /w/store /w/state"}}) + if err == nil { + _, _, _ = docker.Wait(context.Background(), clean) + docker.Remove(context.Background(), clean) + } + _ = os.RemoveAll(dir) + }) + + store, _, err := docker.Start(ctx, Run{Image: image, + Env: []string{"POSTGRES_PASSWORD_FILE=/run/secrets/superuser", "PGDATA=" + pgdata}, + Binds: []string{dir + "/store:/var/lib/postgresql/data", dir + "/state/superuser.secret:/run/secrets/superuser:ro"}}) + if err != nil { + t.Fatal(err) + } + defer docker.Remove(context.Background(), store) + answered := false + for i := 0; i < 90 && !answered; i++ { + // Over TCP, and with the password: the temporary server initdb runs listens on the socket only. + code, err := docker.Exec(ctx, store, "sh", "-c", + "PGPASSWORD=measured-pw psql -h 127.0.0.1 -U postgres -tAc 'select 1' | grep -qx 1") + answered = err == nil && code == 0 + if !answered { + time.Sleep(2 * time.Second) + } + } + if !answered { + t.Fatalf("%s did not initialise a store that answers on a directory made %s %s, as genesis makes it:\n%s", + image, owner, orMode(mode), lastLinesOf(docker.Logs(ctx, store), 15)) + } +} + +func orMode(m string) string { + if m == "" { + return "0700" + } + return m +} + +func lastLinesOf(s string, n int) string { + lines := strings.Split(strings.TrimRight(s, "\n"), "\n") + if len(lines) > n { + lines = lines[len(lines)-n:] + } + return strings.Join(lines, "\n") +}