Files

247 lines
12 KiB
Bash

#!/bin/dash
# Initialize or reuse a local PostgreSQL cluster for development workflows.
#
# Public contract:
# - Requires either PG_WORKING_DIR or LOCAL_DIR.
# - Produces a ready-to-use database and exports:
# POSTGRESQL_HOST, POSTGRESQL_PORT, POSTGRESQL_USER, POSTGRESQL_DATABASE,
# PGURL, and also the variable named by PG_URL_VAR.
# - Reuses an existing cluster only when PG_REUSE is set and PG_VERSION exists.
# - If a reused cluster cannot start, it is reinitialized automatically.
#
# Important knobs:
# - PG_WORKING_DIR: cluster root (defaults to $LOCAL_DIR/focus/postgresql)
# - PG_DATABASE: database to create/ensure (default: testdb)
# - PG_PORT: socket port (default: 5432)
# - PG_URL_VAR: alternate URL variable to export in addition to PGURL
# - PG_CONF_FILE: base postgresql.conf to copy on fresh init
# - PG_SHARED_PRELOAD_LIBRARIES: preload list; empty string disables pg_cron
# - PG_DISABLE_LOGGING=1: disable logging_collector in generated config
# - NO_TTY=1: redirect pg_ctl stop/start output into temp log files
postgres_init_help() {
cat <<'EOF'
Usage: postgres-init
Initialize or reuse a local PostgreSQL cluster.
Environment:
PG_WORKING_DIR Cluster root directory
LOCAL_DIR Fallback root used to derive PG_WORKING_DIR
PG_DATABASE Database name to create/ensure (default: testdb)
PG_PORT PostgreSQL port / unix socket port (default: 5432)
PG_URL_VAR Extra URL variable to export alongside PGURL
PG_CONF_FILE Base postgresql.conf copied on fresh init only
PG_SHARED_PRELOAD_LIBRARIES Preload list; empty disables pg_cron preload
PG_DISABLE_LOGGING Set to 1 to disable logging_collector
PG_REUSE Reuse existing cluster if PG_VERSION exists
NO_TTY Redirect pg_ctl output to temp files
Behavior:
- Rewrites runtime socket/port/cron settings on every launch.
- Creates the target database if missing.
- If a reused cluster exists but cannot start, reinitializes it automatically.
EOF
}
postgres_init_main() {
case "${1:-}" in
-h|--help)
postgres_init_help
return 0
;;
esac
if [ -z "${PG_WORKING_DIR:-}" ] && [ -z "${LOCAL_DIR:-}" ]; then
printf '%s\n' 'postgres-init: PG_WORKING_DIR or LOCAL_DIR is required' >&2
return 1
fi
: "${PG_WORKING_DIR:=$LOCAL_DIR/focus/postgresql}"
: "${PG_PORT:=5432}"
: "${PG_DATABASE:=testdb}"
: "${PG_DISABLE_LOGGING:=0}"
[ "${PG_SHARED_PRELOAD_LIBRARIES+x}" ] || PG_SHARED_PRELOAD_LIBRARIES='pg_cron'
: "${PG_URL_VAR:=PGURL}"
mkdir -p "$PG_WORKING_DIR" || return 1
wd="$PG_WORKING_DIR"; data="$wd/data"; sockdir="$wd/sock"; db="$PG_DATABASE"
if [ "${NO_TTY:-0}" = "1" ]; then
_pg_log="$(mktemp /tmp/postgres-init-stop.XXXXXX.log)"
pg_ctl -D "$data" -m fast -w stop >"$_pg_log" 2>&1 || :
printf '%s\n' "postgres-init: pg_ctl stop output redirected to $_pg_log" >&2
else
pg_ctl -D "$data" -m fast -w stop || :
fi
mkdir -p "$sockdir" || return 1
# Reuse is optimistic: if a cluster exists on disk, try to start it first.
# We only destroy state after a real startup failure proves the cluster is
# broken, which keeps long-lived local DBs intact when possible.
if [ "${PG_REUSE+x}" ] && [ -f "$data/PG_VERSION" ]; then PG_REUSE=1; else PG_REUSE=0; fi
if [ "$PG_REUSE" -eq 0 ]; then
rm -rf "$data" "$sockdir" || return 1
mkdir -p "$sockdir" || return 1
initdb -D "$data" --no-locale -E UTF8 || return 1
if [ -n "${PG_CONF_FILE:-}" ]; then
# PG_CONF_FILE replaces the base postgresql.conf on fresh init only. We
# still append runtime values later so the helper can relocate sockets,
# ports, and cron settings regardless of the custom file contents.
[ -r "$PG_CONF_FILE" ] || { printf '%s\n' "postgres-init: PG_CONF_FILE not readable: $PG_CONF_FILE" >&2; return 1; }
cp -f -- "$PG_CONF_FILE" "$data/postgresql.conf" || return 1
else
{
printf '%s\n' "listen_addresses = ''"
if [ "$PG_DISABLE_LOGGING" -eq 0 ]; then
printf '%s\n' 'logging_collector = on'
printf '%s\n' "log_directory = 'log'"
fi
if [ -n "$PG_SHARED_PRELOAD_LIBRARIES" ]; then
printf '%s\n' "shared_preload_libraries = '$PG_SHARED_PRELOAD_LIBRARIES'"
printf '%s\n' "cron.database_name = '$db'"
printf '%s\n' "cron.host = '$sockdir'"
fi
} >> "$data/postgresql.conf" || return 1
fi
sed -i "1ilocal all all trust" "$data/pg_hba.conf" || return 1
fi
# Rewrite the runtime knobs on every launch. Reused clusters may have been
# started from another workspace/session, so we must override stale socket,
# port, and pg_cron wiring before attempting startup.
[ -f "$data/postgresql.auto.conf" ] || : > "$data/postgresql.auto.conf"
[ -f "$data/pg_ident.conf" ] || : > "$data/pg_ident.conf"
sed -i '/^[[:space:]]*port[[:space:]]*=/d' "$data/postgresql.conf" || return 1
sed -i '/^[[:space:]]*unix_socket_directories[[:space:]]*=/d' "$data/postgresql.conf" || return 1
sed -i '/^[[:space:]]*port[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*unix_socket_directories[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*hba_file[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*ident_file[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*shared_preload_libraries[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*cron\.database_name[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*cron\.host[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
{ printf '%s\n' "port = $PG_PORT"; printf '%s\n' "unix_socket_directories = '$sockdir'"; } >> "$data/postgresql.conf" || return 1
{
printf '%s\n' "port = '$PG_PORT'"
printf '%s\n' "unix_socket_directories = '$sockdir'"
printf '%s\n' "hba_file = '$data/pg_hba.conf'"
printf '%s\n' "ident_file = '$data/pg_ident.conf'"
if [ "$PG_DISABLE_LOGGING" -eq 0 ]; then
printf '%s\n' "logging_collector = 'on'"
else
printf '%s\n' "logging_collector = 'off'"
fi
if [ -n "$PG_SHARED_PRELOAD_LIBRARIES" ]; then
printf '%s\n' "shared_preload_libraries = '$PG_SHARED_PRELOAD_LIBRARIES'"
printf '%s\n' "cron.database_name = '$db'"
printf '%s\n' "cron.host = '$sockdir'"
fi
} >> "$data/postgresql.auto.conf" || return 1
_pg_start_exit=0
if [ "${NO_TTY:-0}" = "1" ]; then
_pg_log="$(mktemp /tmp/postgres-init-start.XXXXXX.log)"
with_closed_fds pg_ctl -D "$data" -o "-F" -w start >"$_pg_log" 2>&1 || _pg_start_exit=$?
printf '%s\n' "postgres-init: pg_ctl start output redirected to $_pg_log" >&2
else
with_closed_fds pg_ctl -D "$data" -o "-F" -w start || _pg_start_exit=$?
fi
if [ "$_pg_start_exit" -ne 0 ]; then
if [ "$PG_REUSE" -eq 1 ]; then
# Self-heal path: the on-disk cluster exists but cannot complete startup
# (for example bad WAL / invalid checkpoint). At this point preserving
# the broken state is less useful than reinitializing automatically.
printf '%s\n' "postgres-init: reused cluster failed to start; reinitializing $wd" >&2
PG_REUSE=0
rm -rf "$data" "$sockdir" || return 1
mkdir -p "$sockdir" || return 1
initdb -D "$data" --no-locale -E UTF8 || return 1
if [ -n "${PG_CONF_FILE:-}" ]; then
[ -r "$PG_CONF_FILE" ] || { printf '%s\n' "postgres-init: PG_CONF_FILE not readable: $PG_CONF_FILE" >&2; return 1; }
cp -f -- "$PG_CONF_FILE" "$data/postgresql.conf" || return 1
else
{
printf '%s\n' "listen_addresses = ''"
if [ "$PG_DISABLE_LOGGING" -eq 0 ]; then
printf '%s\n' 'logging_collector = on'
printf '%s\n' "log_directory = 'log'"
fi
if [ -n "$PG_SHARED_PRELOAD_LIBRARIES" ]; then
printf '%s\n' "shared_preload_libraries = '$PG_SHARED_PRELOAD_LIBRARIES'"
printf '%s\n' "cron.database_name = '$db'"
printf '%s\n' "cron.host = '$sockdir'"
fi
} >> "$data/postgresql.conf" || return 1
fi
sed -i "1ilocal all all trust" "$data/pg_hba.conf" || return 1
[ -f "$data/postgresql.auto.conf" ] || : > "$data/postgresql.auto.conf"
[ -f "$data/pg_ident.conf" ] || : > "$data/pg_ident.conf"
sed -i '/^[[:space:]]*port[[:space:]]*=/d' "$data/postgresql.conf" || return 1
sed -i '/^[[:space:]]*unix_socket_directories[[:space:]]*=/d' "$data/postgresql.conf" || return 1
sed -i '/^[[:space:]]*port[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*unix_socket_directories[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*hba_file[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*ident_file[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*shared_preload_libraries[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*cron\.database_name[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
sed -i '/^[[:space:]]*cron\.host[[:space:]]*=/d' "$data/postgresql.auto.conf" || return 1
{ printf '%s\n' "port = $PG_PORT"; printf '%s\n' "unix_socket_directories = '$sockdir'"; } >> "$data/postgresql.conf" || return 1
{
printf '%s\n' "port = '$PG_PORT'"
printf '%s\n' "unix_socket_directories = '$sockdir'"
printf '%s\n' "hba_file = '$data/pg_hba.conf'"
printf '%s\n' "ident_file = '$data/pg_ident.conf'"
if [ "$PG_DISABLE_LOGGING" -eq 0 ]; then
printf '%s\n' "logging_collector = 'on'"
else
printf '%s\n' "logging_collector = 'off'"
fi
if [ -n "$PG_SHARED_PRELOAD_LIBRARIES" ]; then
printf '%s\n' "shared_preload_libraries = '$PG_SHARED_PRELOAD_LIBRARIES'"
printf '%s\n' "cron.database_name = '$db'"
printf '%s\n' "cron.host = '$sockdir'"
fi
} >> "$data/postgresql.auto.conf" || return 1
_pg_start_exit=0
if [ "${NO_TTY:-0}" = "1" ]; then
_pg_log="$(mktemp /tmp/postgres-init-start.XXXXXX.log)"
with_closed_fds pg_ctl -D "$data" -o "-F" -w start >"$_pg_log" 2>&1 || _pg_start_exit=$?
printf '%s\n' "postgres-init: pg_ctl start output redirected to $_pg_log" >&2
else
with_closed_fds pg_ctl -D "$data" -o "-F" -w start || _pg_start_exit=$?
fi
[ "$_pg_start_exit" -eq 0 ] || return 2
printf '%s\n' "postgres-init: reinitialized broken cluster at $wd" >&2
else
return 2
fi
fi
user="$(id -un)" || return 1
if [ "$PG_REUSE" -eq 0 ]; then
createdb -h "$sockdir" -U "$user" "$db" || return 1
else
if ! psql -h "$sockdir" -p "$PG_PORT" -U "$user" -d postgres -tAc "select 1 from pg_database where datname = '$db'" 2>/dev/null | grep -q '^1$'; then
createdb -h "$sockdir" -U "$user" "$db" || return 1
fi
fi
psql -h "$sockdir" -p "$PG_PORT" -d "$db" -v ON_ERROR_STOP=1 -c 'select 1;' || return 1
export POSTGRESQL_HOST="$sockdir" POSTGRESQL_PORT="$PG_PORT" POSTGRESQL_USER="$user" POSTGRESQL_DATABASE="$db"
# Export both names for compatibility: some callers consume plain PGURL,
# while multi-cluster shells also need a project-specific variable like
# WIPE_PGURL or BMSEARCH_PGURL in the same environment.
PGURL="postgresql://${POSTGRESQL_USER}@/${POSTGRESQL_DATABASE}?host=${POSTGRESQL_HOST}&port=${POSTGRESQL_PORT}"
export PGURL
case $PG_URL_VAR in ''|*[!ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_]* ) printf '%s\n' 'postgres-init: invalid PG_URL_VAR' >&2; return 1 ;; esac
export "${PG_URL_VAR}=${PGURL}" || return 1
return 0
}
postgres_init_main "$@"