Measure that the postgres module's image initialises where genesis now raises the store (hq issue 609, ADR 0338 §5)
mesh/merge-gate head ba2806b985ce merged into trunk 9497dda9291c — pass: the change touches no module of the mesh's graph
mesh/repo-check head ba2806b985ce merged into trunk 9497dda9291c — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/delivery delivered
mesh/merge-gate head ba2806b985ce merged into trunk 9497dda9291c — pass: the change touches no module of the mesh's graph
mesh/repo-check head ba2806b985ce merged into trunk 9497dda9291c — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/delivery delivered
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.
This commit is contained in:
+2
-1
@@ -109,6 +109,7 @@ func (d *Docker) RemoveNetwork(ctx context.Context, id string) {
|
|||||||
type Run struct {
|
type Run struct {
|
||||||
Image string
|
Image string
|
||||||
Cmd []string
|
Cmd []string
|
||||||
|
Env []string
|
||||||
Binds []string
|
Binds []string
|
||||||
Network string
|
Network string
|
||||||
DNS []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)
|
path += "?name=" + url.QueryEscape(r.Name)
|
||||||
}
|
}
|
||||||
if err := d.call(ctx, http.MethodPost, path, map[string]any{"Image": r.Image, "Cmd": r.Cmd,
|
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
|
return "", "", err
|
||||||
}
|
}
|
||||||
if err := d.call(ctx, http.MethodPost, "/containers/"+made.ID+"/start", nil, nil); err != nil {
|
if err := d.call(ctx, http.MethodPost, "/containers/"+made.ID+"/start", nil, nil); err != nil {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user