101 lines
4.4 KiB
Go
101 lines
4.4 KiB
Go
package main
|
|
|
|
// The integration level's throwaway store (novox/hq ADR 0470 §1; issue 1486): one postgres server of the installed
|
|
// package for each run, `initdb` into the run's own directory, listening on a unix socket in that directory and on no
|
|
// TCP address, as the module's account, with durability off as today's check sets it.
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
|
)
|
|
|
|
// storeStopBound is how long stopping the store is given: it runs after the level, whatever the level did.
|
|
const storeStopBound = 30 * time.Second
|
|
|
|
var postgresVersion = regexp.MustCompile(`PostgreSQL\)\s+(\d+)`)
|
|
|
|
// postgresMajor is the major version of the installed postgres: `postgres --version` ("postgres (PostgreSQL) 18.6").
|
|
func (m *Module) postgresMajor(ctx context.Context) (int, error) {
|
|
r := m.Run(ctx, nil, "postgres", "--version")
|
|
if r.Status != 0 || r.Err != "" {
|
|
return 0, fmt.Errorf("`postgres --version`: %s", firstLine(r.Err+" "+r.Stderr))
|
|
}
|
|
f := postgresVersion.FindStringSubmatch(r.Stdout)
|
|
if f == nil {
|
|
return 0, fmt.Errorf("`postgres --version` answered %q", firstLine(r.Stdout))
|
|
}
|
|
return strconv.Atoi(f[1])
|
|
}
|
|
|
|
// checkPostgres refuses the integration level when the installed postgres major is not the recipe's, naming both
|
|
// (ADR 0470 §4); the unit level is not refused. It answers the major it found.
|
|
func (m *Module) checkPostgres(ctx context.Context) (int, error) {
|
|
major, err := m.postgresMajor(ctx)
|
|
if err != nil {
|
|
return 0, &buildstep.Refusal{Text: fmt.Sprintf("the installed postgres version could not be read: %v", err), Want: strconv.Itoa(m.Recipe.PostgresMajor)}
|
|
}
|
|
if major != m.Recipe.PostgresMajor {
|
|
return major, &buildstep.Refusal{Text: fmt.Sprintf("postgres %d installed, the integration level is built for postgres %d", major, m.Recipe.PostgresMajor),
|
|
Want: strconv.Itoa(m.Recipe.PostgresMajor), Have: strconv.Itoa(major)}
|
|
}
|
|
return major, nil
|
|
}
|
|
|
|
// socketLimit is the longest path of a socket postgres accepts, with the file's name: it refuses 108 bytes and over.
|
|
const socketLimit = 100
|
|
|
|
// socketName is the file postgres makes in the socket directory.
|
|
const socketName = "/.s.PGSQL.5432"
|
|
|
|
// socketDir makes the directory of the store's socket: under the run when the socket's path stays under socketLimit,
|
|
// else a short directory of its own under /tmp (answering short, so the caller removes it). A unix socket's path is
|
|
// bounded far below a file's, and a work directory deep in a data root is over it.
|
|
func socketDir(run string) (dir string, short bool, err error) {
|
|
dir = filepath.Join(run, "pg", "socket")
|
|
if len(dir)+len(socketName) < socketLimit {
|
|
return dir, false, os.MkdirAll(dir, 0o700)
|
|
}
|
|
dir, err = os.MkdirTemp("/tmp", "pg")
|
|
return dir, true, err
|
|
}
|
|
|
|
// storeSettings are the server's own settings: no TCP address, a socket directory, and no durability.
|
|
func storeOptions(socket string) string {
|
|
return "-k " + socket + " -c listen_addresses='' -c fsync=off -c synchronous_commit=off -c full_page_writes=off"
|
|
}
|
|
|
|
// startStore makes and starts the run's postgres in run/pg and answers the address tests are given as
|
|
// MESH_TEST_POSTGRES, and what stops it. The address names the socket directory percent-encoded, as the controller's
|
|
// tests cut the URL at its last slash to name their own databases.
|
|
func (m *Module) startStore(ctx context.Context, run string) (string, func(), error) {
|
|
data := filepath.Join(run, "pg", "data")
|
|
socket, short, err := socketDir(run)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
stop := func() {
|
|
if short {
|
|
defer os.RemoveAll(socket)
|
|
}
|
|
sctx, cancel := context.WithTimeout(context.Background(), storeStopBound)
|
|
defer cancel()
|
|
m.Run(sctx, nil, "pg_ctl", "-D", data, "-m", "immediate", "-w", "stop")
|
|
}
|
|
if r := m.Run(ctx, nil, "initdb", "-D", data, "-U", "postgres", "--auth=trust", "-E", "UTF8", "--no-locale"); r.Status != 0 || r.Err != "" {
|
|
return "", nil, fmt.Errorf("initdb: %s", firstLine(r.Err+" "+r.Stderr+" "+r.Stdout))
|
|
}
|
|
if r := m.Run(ctx, nil, "pg_ctl", "-D", data, "-l", filepath.Join(run, "pg", "log"), "-w", "-t", "30", "-o", storeOptions(socket), "start"); r.Status != 0 || r.Err != "" {
|
|
stop() // a server that was half started is stopped
|
|
return "", nil, fmt.Errorf("pg_ctl start: %s", firstLine(r.Err+" "+r.Stderr+" "+r.Stdout))
|
|
}
|
|
return "postgres://postgres@/postgres?host=" + url.QueryEscape(socket) + "&sslmode=disable", stop, nil
|
|
}
|