Support host PostgreSQL through Unix sockets
Some checks are pending
Quality / full-local-gates (push) Waiting to run
Some checks are pending
Quality / full-local-gates (push) Waiting to run
This commit is contained in:
parent
585f5648ff
commit
27c615d6cb
|
|
@ -100,6 +100,10 @@ POSTGRES_DB=who_need_help
|
|||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=replace-with-a-local-or-deployment-secret
|
||||
DATABASE_URL=ecto://postgres:replace-with-url-encoded-password@db/who_need_help
|
||||
# Optional absolute host directory containing PostgreSQL Unix sockets. When it
|
||||
# is set in external mode, Compose mounts it read-only and Ecto uses it instead
|
||||
# of the hostname in DATABASE_URL. Leave empty for container or remote TCP DBs.
|
||||
DATABASE_SOCKET_DIR=
|
||||
|
||||
WEB_POOL_SIZE=4
|
||||
WORKER_POOL_SIZE=2
|
||||
|
|
|
|||
22
README.md
22
README.md
|
|
@ -238,6 +238,28 @@ reports whether the observed connection uses TLS without printing the URL or
|
|||
credentials. Provider-specific CA/network requirements still have to be
|
||||
configured from that provider's verified documentation.
|
||||
|
||||
For PostgreSQL installed on the same Linux host, keep its TCP listener private
|
||||
and connect through its Unix socket. The root-only bootstrap refuses existing
|
||||
project roles/databases, backs up `pg_hba.conf`, adds two exact SCRAM rules,
|
||||
creates independent production/staging roles and empty databases, preloads
|
||||
`citext` and PostGIS, verifies both logins, and writes mode-`0600` initializer
|
||||
fragments without printing their passwords:
|
||||
|
||||
```bash
|
||||
sudo ./scripts/provision-host-postgres.sh "$USER"
|
||||
set -a
|
||||
. "$HOME/.config/who_need_help/database-production.env"
|
||||
set +a
|
||||
./scripts/init-production-env.sh whoneedhelp.com .env.production
|
||||
unset PRODUCTION_DATABASE_MODE PRODUCTION_DATABASE_URL \
|
||||
PRODUCTION_DATABASE_SOCKET_DIR
|
||||
```
|
||||
|
||||
Use `database-staging.env` for the isolated staging environment. Compose mounts
|
||||
only the configured socket directory read-only; Ecto migrations remain the
|
||||
source of application schema. Inspect the exact host PostgreSQL state and the
|
||||
script's documented impact before the sudo invocation.
|
||||
|
||||
`compose.production.yaml` leaves local Mailpit stopped. Validation deliberately
|
||||
fails while the relay still points to Mailpit or a template marker remains.
|
||||
It does not claim to test DNS, certificates, actual mail delivery, the
|
||||
|
|
|
|||
16
compose.external-db-socket.yaml
Normal file
16
compose.external-db-socket.yaml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
services:
|
||||
migrate:
|
||||
volumes:
|
||||
- ${DATABASE_SOCKET_DIR:?Set DATABASE_SOCKET_DIR for a host PostgreSQL Unix socket}:${DATABASE_SOCKET_DIR}:ro
|
||||
|
||||
app:
|
||||
volumes:
|
||||
- ${DATABASE_SOCKET_DIR:?Set DATABASE_SOCKET_DIR for a host PostgreSQL Unix socket}:${DATABASE_SOCKET_DIR}:ro
|
||||
|
||||
web:
|
||||
volumes:
|
||||
- ${DATABASE_SOCKET_DIR:?Set DATABASE_SOCKET_DIR for a host PostgreSQL Unix socket}:${DATABASE_SOCKET_DIR}:ro
|
||||
|
||||
worker:
|
||||
volumes:
|
||||
- ${DATABASE_SOCKET_DIR:?Set DATABASE_SOCKET_DIR for a host PostgreSQL Unix socket}:${DATABASE_SOCKET_DIR}:ro
|
||||
|
|
@ -3,6 +3,7 @@ name: who_need_help
|
|||
x-app-environment: &app-environment
|
||||
APP_ROLE: web
|
||||
DATABASE_URL: ${DATABASE_URL:?Set DATABASE_URL in .env}
|
||||
DATABASE_SOCKET_DIR: ${DATABASE_SOCKET_DIR:-}
|
||||
SECRET_KEY_BASE: ${SECRET_KEY_BASE:?Set SECRET_KEY_BASE in .env}
|
||||
HANDOVER_SECRET: ${HANDOVER_SECRET:?Set HANDOVER_SECRET in .env}
|
||||
RELEASE_COOKIE: ${RELEASE_COOKIE:?Set RELEASE_COOKIE in .env}
|
||||
|
|
|
|||
|
|
@ -339,13 +339,33 @@ if config_env() == :prod do
|
|||
|
||||
maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
|
||||
|
||||
config :who_need_help, WhoNeedHelp.Repo,
|
||||
# ssl: true,
|
||||
url: database_url,
|
||||
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
|
||||
# For machines with several cores, consider starting multiple pools of `pool_size`
|
||||
# pool_count: 4,
|
||||
socket_options: maybe_ipv6
|
||||
database_socket_dir =
|
||||
case System.get_env("DATABASE_SOCKET_DIR") do
|
||||
value when value in [nil, ""] ->
|
||||
nil
|
||||
|
||||
"/" <> _rest = value ->
|
||||
value
|
||||
|
||||
_other ->
|
||||
raise "DATABASE_SOCKET_DIR must be an absolute path when configured."
|
||||
end
|
||||
|
||||
repo_options =
|
||||
[
|
||||
url: database_url,
|
||||
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
|
||||
socket_options: maybe_ipv6
|
||||
]
|
||||
|> then(fn options ->
|
||||
if database_socket_dir do
|
||||
Keyword.put(options, :socket_dir, database_socket_dir)
|
||||
else
|
||||
options
|
||||
end
|
||||
end)
|
||||
|
||||
config :who_need_help, WhoNeedHelp.Repo, repo_options
|
||||
|
||||
# The secret key base is used to sign/encrypt cookies and other secrets.
|
||||
# A default value is used in config/dev.exs and config/test.exs but you
|
||||
|
|
|
|||
|
|
@ -161,6 +161,59 @@ run-scoped account.
|
|||
|
||||
### External PostgreSQL/PostGIS
|
||||
|
||||
#### PostgreSQL on the same Linux host
|
||||
|
||||
The server-local mode uses the PostgreSQL Unix socket instead of exposing the
|
||||
database on a Docker-reachable TCP address. Ecto/Postgrex receive
|
||||
`DATABASE_SOCKET_DIR`; `scripts/compose.sh` then adds
|
||||
`compose.external-db-socket.yaml` and mounts that exact directory read-only
|
||||
into the active application and migration services. An empty setting preserves
|
||||
the normal remote-provider TCP behavior.
|
||||
|
||||
After read-only inspection confirms the intended PostgreSQL 18 cluster,
|
||||
installed PostGIS package, socket directory, and absence of the project-scoped
|
||||
roles/databases, run the root-only provisioner:
|
||||
|
||||
```bash
|
||||
sudo ./scripts/provision-host-postgres.sh "$USER"
|
||||
```
|
||||
|
||||
Its exact mutation scope is:
|
||||
|
||||
- prepend two database-and-role-specific `local ... scram-sha-256` rules to the
|
||||
active `pg_hba.conf`, retaining a mode-`0600` copy under
|
||||
`/var/backups/who_need_help/`, and reload that cluster;
|
||||
- create login roles `wnh_production` and `wnh_staging` without superuser,
|
||||
database-creation, role-creation, replication, or row-security bypass rights;
|
||||
- create empty owner databases `who_need_help_production` and
|
||||
`who_need_help_staging`, revoke public connect, and preload `citext` and
|
||||
PostGIS so the non-superuser Ecto migrations can run;
|
||||
- verify both credentials through the observed Unix socket and write separate
|
||||
mode-`0600` initializer fragments under
|
||||
`~/.config/who_need_help/` without printing passwords.
|
||||
|
||||
The command refuses to overwrite credential fragments, refuses any matching
|
||||
pre-existing role/database or managed HBA marker, validates the candidate HBA
|
||||
rules before reload, and restores its HBA backup plus removes only objects it
|
||||
created if provisioning fails. It does not inspect or migrate application
|
||||
tables; the release migration runner remains authoritative for schema.
|
||||
|
||||
Generate staging or production after loading only the matching trusted
|
||||
fragment:
|
||||
|
||||
```bash
|
||||
set -a
|
||||
. "$HOME/.config/who_need_help/database-production.env"
|
||||
set +a
|
||||
./scripts/init-production-env.sh whoneedhelp.com .env.production
|
||||
unset PRODUCTION_DATABASE_MODE PRODUCTION_DATABASE_URL \
|
||||
PRODUCTION_DATABASE_SOCKET_DIR
|
||||
```
|
||||
|
||||
For staging, load `database-staging.env` and pass the staging domain, project,
|
||||
upstream alias, and HTTP port described above. Do not load both fragments into
|
||||
one shell.
|
||||
|
||||
Provision the database and role first, then generate the environment without
|
||||
placing its credentials on a command line that is retained in shell history:
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ esac
|
|||
|
||||
database_url=$(read_env_value DATABASE_URL 2>/dev/null || true)
|
||||
: "${database_url:?Set DATABASE_URL in $env_file}"
|
||||
database_socket_dir=$(read_env_value DATABASE_SOCKET_DIR 2>/dev/null || true)
|
||||
|
||||
if [[ "$database_mode" == container ]]; then
|
||||
postgres_db=$(read_env_value POSTGRES_DB 2>/dev/null || true)
|
||||
|
|
@ -86,6 +87,10 @@ compose=(
|
|||
|
||||
if [[ "$database_mode" == external ]]; then
|
||||
compose+=(--file "$ROOT/compose.external-db.yaml")
|
||||
|
||||
if [[ -n "$database_socket_dir" ]]; then
|
||||
compose+=(--file "$ROOT/compose.external-db-socket.yaml")
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$app_topology" == compact ]]; then
|
||||
|
|
|
|||
|
|
@ -83,6 +83,19 @@ case "$database_mode" in
|
|||
;;
|
||||
esac
|
||||
|
||||
database_socket_dir=${PRODUCTION_DATABASE_SOCKET_DIR:-}
|
||||
if [ -n "$database_socket_dir" ]; then
|
||||
case "$database_socket_dir" in
|
||||
/*) ;;
|
||||
*) echo "PRODUCTION_DATABASE_SOCKET_DIR must be an absolute path." >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
if [ "$database_mode" != external ]; then
|
||||
echo "PRODUCTION_DATABASE_SOCKET_DIR is only valid when PRODUCTION_DATABASE_MODE=external." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$app_topology" in
|
||||
compact | split) ;;
|
||||
*) echo "PRODUCTION_APP_TOPOLOGY must be compact or split." >&2; exit 1 ;;
|
||||
|
|
@ -129,6 +142,7 @@ DOCKER_SOCKET_GID_VALUE=$docker_socket_gid \
|
|||
TRUSTED_PROXY_IPS_VALUE=$trusted_proxy_ips \
|
||||
POSTGRES_PASSWORD_VALUE=$postgres_password \
|
||||
DATABASE_URL_VALUE=$database_url \
|
||||
DATABASE_SOCKET_DIR_VALUE=$database_socket_dir \
|
||||
SECRET_KEY_BASE_VALUE=$secret_key_base \
|
||||
HANDOVER_SECRET_VALUE=$handover_secret \
|
||||
RELEASE_COOKIE_VALUE=$release_cookie \
|
||||
|
|
@ -166,6 +180,7 @@ CODEX_SESSION_ID_VALUE=$codex_session_id \
|
|||
replacement["WNH_BASE_URL"] = "https://" ENVIRON["DOMAIN"]
|
||||
replacement["POSTGRES_PASSWORD"] = ENVIRON["POSTGRES_PASSWORD_VALUE"]
|
||||
replacement["DATABASE_URL"] = ENVIRON["DATABASE_URL_VALUE"]
|
||||
replacement["DATABASE_SOCKET_DIR"] = ENVIRON["DATABASE_SOCKET_DIR_VALUE"]
|
||||
replacement["SECRET_KEY_BASE"] = ENVIRON["SECRET_KEY_BASE_VALUE"]
|
||||
replacement["HANDOVER_SECRET"] = ENVIRON["HANDOVER_SECRET_VALUE"]
|
||||
replacement["RELEASE_COOKIE"] = ENVIRON["RELEASE_COOKIE_VALUE"]
|
||||
|
|
|
|||
242
scripts/provision-host-postgres.sh
Executable file
242
scripts/provision-host-postgres.sh
Executable file
|
|
@ -0,0 +1,242 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$(id -u)" != 0 ]]; then
|
||||
echo "Run this script as root through sudo on the PostgreSQL host." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
operator=${1:-${SUDO_USER:-}}
|
||||
if [[ -z "$operator" || "$operator" == root ]]; then
|
||||
echo "Usage: sudo $0 OPERATOR_USER [OUTPUT_DIRECTORY]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
operator_entry=$(getent passwd "$operator") || {
|
||||
echo "Operator user does not exist: $operator" >&2
|
||||
exit 1
|
||||
}
|
||||
operator_home=$(cut -d: -f6 <<<"$operator_entry")
|
||||
operator_group=$(id -gn "$operator")
|
||||
output_dir=${2:-"$operator_home/.config/who_need_help"}
|
||||
|
||||
postgres_version=${POSTGRES_CLUSTER_VERSION:-18}
|
||||
postgres_cluster=${POSTGRES_CLUSTER_NAME:-main}
|
||||
socket_dir=${POSTGRES_SOCKET_DIR:-/var/run/postgresql}
|
||||
|
||||
production_role=wnh_production
|
||||
production_database=who_need_help_production
|
||||
staging_role=wnh_staging
|
||||
staging_database=who_need_help_staging
|
||||
hba_marker="# BEGIN Who Need Help managed local socket authentication"
|
||||
|
||||
for command in awk cat chgrp chmod chown cut date getent grep id install mktemp \
|
||||
openssl pg_ctlcluster pg_lsclusters psql rm runuser sed stat tr; do
|
||||
command -v "$command" >/dev/null 2>&1 || {
|
||||
echo "Required command is unavailable: $command" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
cluster_status=$(
|
||||
pg_lsclusters --no-header |
|
||||
awk -v version="$postgres_version" -v cluster="$postgres_cluster" \
|
||||
'$1 == version && $2 == cluster {print $4}'
|
||||
)
|
||||
if [[ "$cluster_status" != online ]]; then
|
||||
echo "PostgreSQL cluster $postgres_version/$postgres_cluster is not online." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
postgres_psql=(
|
||||
runuser -u postgres --
|
||||
psql --no-psqlrc --set ON_ERROR_STOP=1 --quiet --tuples-only --no-align
|
||||
)
|
||||
|
||||
hba_file=$("${postgres_psql[@]}" --dbname postgres --command 'SHOW hba_file')
|
||||
configured_socket_dirs=$(
|
||||
"${postgres_psql[@]}" --dbname postgres --command 'SHOW unix_socket_directories'
|
||||
)
|
||||
postgres_port=$("${postgres_psql[@]}" --dbname postgres --command 'SHOW port')
|
||||
|
||||
[[ "$hba_file" == /* && -f "$hba_file" ]] || {
|
||||
echo "PostgreSQL reported an unusable hba_file path." >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$socket_dir" == /* && -d "$socket_dir" ]] || {
|
||||
echo "PostgreSQL socket directory is unavailable: $socket_dir" >&2
|
||||
exit 1
|
||||
}
|
||||
if ! tr ',' '\n' <<<"$configured_socket_dirs" |
|
||||
sed -e "s/^[[:space:]']*//" -e "s/[[:space:]']*$//" |
|
||||
grep -Fx "$socket_dir" >/dev/null; then
|
||||
echo "POSTGRES_SOCKET_DIR is not listed in unix_socket_directories." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
socket_path="$socket_dir/.s.PGSQL.$postgres_port"
|
||||
[[ -S "$socket_path" ]] || {
|
||||
echo "PostgreSQL Unix socket is unavailable: $socket_path" >&2
|
||||
exit 1
|
||||
}
|
||||
socket_mode=$(stat -c '%a' "$socket_path")
|
||||
case "${socket_mode: -1}" in
|
||||
6 | 7) ;;
|
||||
*)
|
||||
echo "PostgreSQL socket is not writable by the unprivileged container user." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if grep -F "$hba_marker" "$hba_file" >/dev/null; then
|
||||
echo "Who Need Help HBA rules already exist; refusing an ambiguous reprovision." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
existing_objects=$(
|
||||
"${postgres_psql[@]}" --dbname postgres --command "
|
||||
SELECT 'role:' || rolname FROM pg_roles
|
||||
WHERE rolname IN ('$production_role', '$staging_role')
|
||||
UNION ALL
|
||||
SELECT 'database:' || datname FROM pg_database
|
||||
WHERE datname IN ('$production_database', '$staging_database')
|
||||
ORDER BY 1;
|
||||
"
|
||||
)
|
||||
if [[ -n "$existing_objects" ]]; then
|
||||
echo "Project-scoped PostgreSQL roles or databases already exist:" >&2
|
||||
while IFS= read -r object; do
|
||||
printf ' %s\n' "$object" >&2
|
||||
done <<<"$existing_objects"
|
||||
echo "Inspect them before deciding whether to reuse, rotate, or remove them." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
production_fragment="$output_dir/database-production.env"
|
||||
staging_fragment="$output_dir/database-staging.env"
|
||||
if [[ -e "$production_fragment" || -e "$staging_fragment" ]]; then
|
||||
echo "Database credential fragments already exist; refusing to overwrite them." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
umask 077
|
||||
production_password=$(openssl rand -hex 32)
|
||||
staging_password=$(openssl rand -hex 32)
|
||||
work_dir=$(mktemp -d)
|
||||
chgrp postgres "$work_dir"
|
||||
chmod 750 "$work_dir"
|
||||
hba_candidate="$work_dir/pg_hba.conf"
|
||||
hba_backup="$work_dir/pg_hba.conf.original"
|
||||
sql_file="$work_dir/provision.sql"
|
||||
backup_dir=/var/backups/who_need_help
|
||||
backup_file="$backup_dir/pg_hba.conf.before-who-need-help-$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
provision_started=false
|
||||
provision_finished=false
|
||||
|
||||
cleanup() {
|
||||
status=$?
|
||||
|
||||
if [[ "$status" != 0 && "$provision_started" == true && "$provision_finished" == false ]]; then
|
||||
"${postgres_psql[@]}" --dbname postgres --command \
|
||||
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname IN ('$production_database', '$staging_database') AND pid <> pg_backend_pid();" \
|
||||
>/dev/null 2>&1 || true
|
||||
"${postgres_psql[@]}" --dbname postgres --command \
|
||||
"DROP DATABASE IF EXISTS $staging_database;" >/dev/null 2>&1 || true
|
||||
"${postgres_psql[@]}" --dbname postgres --command \
|
||||
"DROP DATABASE IF EXISTS $production_database;" >/dev/null 2>&1 || true
|
||||
"${postgres_psql[@]}" --dbname postgres --command \
|
||||
"DROP ROLE IF EXISTS $staging_role; DROP ROLE IF EXISTS $production_role;" \
|
||||
>/dev/null 2>&1 || true
|
||||
|
||||
if [[ -f "$hba_backup" ]]; then
|
||||
install -m 640 -o postgres -g postgres "$hba_backup" "$hba_file" || true
|
||||
pg_ctlcluster "$postgres_version" "$postgres_cluster" reload || true
|
||||
fi
|
||||
|
||||
rm -f "$production_fragment" "$staging_fragment"
|
||||
fi
|
||||
|
||||
rm -rf "$work_dir"
|
||||
unset production_password staging_password
|
||||
exit "$status"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
|
||||
install -m 600 -o root -g root "$hba_file" "$hba_backup"
|
||||
{
|
||||
printf '%s\n' "$hba_marker"
|
||||
printf 'local %s %s scram-sha-256\n' "$production_database" "$production_role"
|
||||
printf 'local %s %s scram-sha-256\n' "$staging_database" "$staging_role"
|
||||
printf '%s\n' '# END Who Need Help managed local socket authentication'
|
||||
cat "$hba_backup"
|
||||
} >"$hba_candidate"
|
||||
|
||||
install -d -m 700 -o root -g root "$backup_dir"
|
||||
install -m 600 -o root -g root "$hba_backup" "$backup_file"
|
||||
install -m 640 -o postgres -g postgres "$hba_candidate" "$hba_file"
|
||||
provision_started=true
|
||||
|
||||
hba_errors=$(
|
||||
"${postgres_psql[@]}" --dbname postgres --command \
|
||||
"SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL;"
|
||||
)
|
||||
if [[ "$hba_errors" != 0 ]]; then
|
||||
echo "PostgreSQL rejected the candidate pg_hba.conf; restoring the backup." >&2
|
||||
exit 1
|
||||
fi
|
||||
pg_ctlcluster "$postgres_version" "$postgres_cluster" reload
|
||||
|
||||
cat >"$sql_file" <<SQL
|
||||
SET password_encryption = 'scram-sha-256';
|
||||
CREATE ROLE $production_role LOGIN PASSWORD '$production_password'
|
||||
NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
|
||||
CREATE ROLE $staging_role LOGIN PASSWORD '$staging_password'
|
||||
NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
|
||||
CREATE DATABASE $production_database OWNER $production_role TEMPLATE template0;
|
||||
CREATE DATABASE $staging_database OWNER $staging_role TEMPLATE template0;
|
||||
REVOKE CONNECT ON DATABASE $production_database FROM PUBLIC;
|
||||
REVOKE CONNECT ON DATABASE $staging_database FROM PUBLIC;
|
||||
GRANT CONNECT ON DATABASE $production_database TO $production_role;
|
||||
GRANT CONNECT ON DATABASE $staging_database TO $staging_role;
|
||||
\connect $production_database
|
||||
CREATE EXTENSION IF NOT EXISTS citext;
|
||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
\connect $staging_database
|
||||
CREATE EXTENSION IF NOT EXISTS citext;
|
||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
SQL
|
||||
chmod 600 "$sql_file"
|
||||
chown postgres:postgres "$sql_file"
|
||||
"${postgres_psql[@]}" --dbname postgres --file "$sql_file"
|
||||
|
||||
PGPASSWORD="$production_password" psql --no-psqlrc --set ON_ERROR_STOP=1 \
|
||||
--host "$socket_dir" --username "$production_role" \
|
||||
--dbname "$production_database" --quiet --tuples-only --no-align \
|
||||
--command 'SELECT current_user, current_database(), PostGIS_Version()' >/dev/null
|
||||
PGPASSWORD="$staging_password" psql --no-psqlrc --set ON_ERROR_STOP=1 \
|
||||
--host "$socket_dir" --username "$staging_role" \
|
||||
--dbname "$staging_database" --quiet --tuples-only --no-align \
|
||||
--command 'SELECT current_user, current_database(), PostGIS_Version()' >/dev/null
|
||||
|
||||
install -d -m 700 -o "$operator" -g "$operator_group" "$output_dir"
|
||||
production_tmp="$work_dir/database-production.env"
|
||||
staging_tmp="$work_dir/database-staging.env"
|
||||
printf '%s\n' \
|
||||
'PRODUCTION_DATABASE_MODE=external' \
|
||||
"PRODUCTION_DATABASE_URL=ecto://$production_role:$production_password@localhost/$production_database" \
|
||||
"PRODUCTION_DATABASE_SOCKET_DIR=$socket_dir" >"$production_tmp"
|
||||
printf '%s\n' \
|
||||
'PRODUCTION_DATABASE_MODE=external' \
|
||||
"PRODUCTION_DATABASE_URL=ecto://$staging_role:$staging_password@localhost/$staging_database" \
|
||||
"PRODUCTION_DATABASE_SOCKET_DIR=$socket_dir" >"$staging_tmp"
|
||||
install -m 600 -o "$operator" -g "$operator_group" "$production_tmp" "$production_fragment"
|
||||
install -m 600 -o "$operator" -g "$operator_group" "$staging_tmp" "$staging_fragment"
|
||||
|
||||
provision_finished=true
|
||||
|
||||
echo "Provisioned isolated production and staging PostgreSQL roles and databases."
|
||||
echo "Verified SCRAM authentication, citext, and PostGIS through $socket_dir."
|
||||
echo "Credential fragments (mode 0600):"
|
||||
echo " $production_fragment"
|
||||
echo " $staging_fragment"
|
||||
echo "Original HBA backup (mode 0600): $backup_file"
|
||||
|
|
@ -153,6 +153,38 @@ PRODUCTION_EMAIL_FROM_ADDRESS=contact@help.test \
|
|||
PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||
./scripts/init-production-env.sh help.test "$external_production_env" >/dev/null
|
||||
./scripts/validate-production-env.sh "$external_production_env" help.test >/dev/null
|
||||
external_socket_production_env="$scan_dir/.env.production.external-db-socket"
|
||||
PRODUCTION_DATABASE_MODE=external \
|
||||
PRODUCTION_DATABASE_URL=ecto://quality:external-password@localhost/who_need_help \
|
||||
PRODUCTION_DATABASE_SOCKET_DIR=/var/run/postgresql \
|
||||
PRODUCTION_SMTP_RELAY=smtp.help.test \
|
||||
PRODUCTION_SMTP_PORT=587 \
|
||||
PRODUCTION_SMTP_USERNAME=quality-user \
|
||||
PRODUCTION_SMTP_PASSWORD=quality-password \
|
||||
PRODUCTION_SMTP_AUTH=always \
|
||||
PRODUCTION_SMTP_TLS=always \
|
||||
PRODUCTION_SMTP_SSL=false \
|
||||
PRODUCTION_EMAIL_FROM_ADDRESS=contact@help.test \
|
||||
PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||
./scripts/init-production-env.sh help.test "$external_socket_production_env" >/dev/null
|
||||
./scripts/validate-production-env.sh \
|
||||
"$external_socket_production_env" help.test >/dev/null
|
||||
if PRODUCTION_DATABASE_MODE=external \
|
||||
PRODUCTION_DATABASE_URL=ecto://quality:external-password@localhost/who_need_help \
|
||||
PRODUCTION_DATABASE_SOCKET_DIR=relative/socket \
|
||||
PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||
./scripts/init-production-env.sh \
|
||||
help.test "$scan_dir/.env.production.invalid-socket" >/dev/null 2>&1; then
|
||||
echo "Production initializer accepted a relative database socket path." >&2
|
||||
exit 1
|
||||
fi
|
||||
if PRODUCTION_DATABASE_SOCKET_DIR=/var/run/postgresql \
|
||||
PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
||||
./scripts/init-production-env.sh \
|
||||
help.test "$scan_dir/.env.production.container-socket" >/dev/null 2>&1; then
|
||||
echo "Production initializer accepted a host socket in container database mode." >&2
|
||||
exit 1
|
||||
fi
|
||||
external_split_production_env="$scan_dir/.env.production.external-db-split"
|
||||
PRODUCTION_APP_TOPOLOGY=split \
|
||||
PRODUCTION_DATABASE_MODE=external \
|
||||
|
|
@ -222,6 +254,7 @@ docker compose --project-directory "$ROOT" --env-file "$edge_env" \
|
|||
./scripts/compose.sh .env.example config --quiet
|
||||
./scripts/compose.sh "$production_env" config --quiet
|
||||
./scripts/compose.sh "$external_production_env" config --quiet
|
||||
./scripts/compose.sh "$external_socket_production_env" config --quiet
|
||||
./scripts/compose.sh "$external_split_production_env" config --quiet
|
||||
./scripts/compose.sh .env.example config --format json |
|
||||
jq --exit-status '
|
||||
|
|
@ -287,6 +320,29 @@ docker compose --project-directory "$ROOT" --env-file "$edge_env" \
|
|||
(.services | has("proxy") | not) and
|
||||
.services.app.networks.public_edge.aliases == ["who-need-help-production"]
|
||||
' >/dev/null
|
||||
./scripts/compose.sh "$external_socket_production_env" config --format json |
|
||||
jq --exit-status '
|
||||
(.services | has("app")) and
|
||||
(.services | has("db") | not) and
|
||||
.services.app.environment.DATABASE_SOCKET_DIR == "/var/run/postgresql" and
|
||||
.services.migrate.environment.DATABASE_SOCKET_DIR == "/var/run/postgresql" and
|
||||
(.services.app.volumes |
|
||||
any(
|
||||
.type == "bind" and
|
||||
.source == "/var/run/postgresql" and
|
||||
.target == "/var/run/postgresql" and
|
||||
.read_only == true
|
||||
)) and
|
||||
(.services.migrate.volumes |
|
||||
any(
|
||||
.type == "bind" and
|
||||
.source == "/var/run/postgresql" and
|
||||
.target == "/var/run/postgresql" and
|
||||
.read_only == true
|
||||
))
|
||||
' >/dev/null
|
||||
./scripts/compose.sh "$external_socket_production_env" config --profiles |
|
||||
grep -Fx container-database >/dev/null
|
||||
./scripts/compose.sh "$external_split_production_env" config --format json |
|
||||
jq --exit-status '
|
||||
(.services | has("db") | not) and
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ public_upstream_name=$(require_value PUBLIC_UPSTREAM_NAME)
|
|||
trusted_proxy_ips=$(optional_value TRAEFIK_TRUSTED_IPS)
|
||||
postgres_password=$(optional_value POSTGRES_PASSWORD)
|
||||
database_url=$(require_value DATABASE_URL)
|
||||
database_socket_dir=$(optional_value DATABASE_SOCKET_DIR)
|
||||
secret_key_base=$(require_value SECRET_KEY_BASE)
|
||||
handover_secret=$(require_value HANDOVER_SECRET)
|
||||
release_cookie=$(require_value RELEASE_COOKIE)
|
||||
|
|
@ -203,6 +204,18 @@ else
|
|||
echo "External DATABASE_URL still targets the Compose db service." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$database_socket_dir" ]]; then
|
||||
[[ "$database_socket_dir" == /* ]] || {
|
||||
echo "DATABASE_SOCKET_DIR must be an absolute path." >&2
|
||||
exit 1
|
||||
}
|
||||
reject_marker DATABASE_SOCKET_DIR "$database_socket_dir"
|
||||
fi
|
||||
fi
|
||||
if [[ "$database_mode" != external && -n "$database_socket_dir" ]]; then
|
||||
echo "DATABASE_SOCKET_DIR is only valid for DATABASE_MODE=external." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[[ "$smtp_relay" != mailpit ]] || {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user