Measure that the postgres module's image initialises where genesis raises the store (issue 609, hq ADR 0338 §5) #99
+2
-1
@@ -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 {
|
||||
|
||||
@@ -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