Project

General

Profile

fwd_create_pg_cluster.sh

Roger Borrello, 08/28/2025 10:33 AM

Download (8.43 KB)

 
1
#!/usr/bin/env bash
2
set -euo pipefail
3

    
4
# set -x
5

    
6
# Defaults
7
PGDATA_DEFAULT="/var/lib/postgresql/data"
8
LOCALE_DEFAULT="en_US.UTF-8"
9
ENCODING_DEFAULT="UTF8"
10
AUTH_DEFAULT="scram-sha-256"   # alternatives: md5 | password | trust
11
TEMP_PORT_DEFAULT="55432"
12
SOCKET_DIR_DEFAULT="/tmp"
13

    
14
# Args
15
PGDATA="$PGDATA_DEFAULT"
16
DB_LOCALE="$LOCALE_DEFAULT"
17
DB_ENCODING="$ENCODING_DEFAULT"
18
AUTH_METHOD="$AUTH_DEFAULT"
19
TEMP_PORT="$TEMP_PORT_DEFAULT"
20
SOCKET_DIR="$SOCKET_DIR_DEFAULT"
21

    
22
POSTGRES_PASSWORD=""
23
ADMIN_USER="fwd_admin"
24
ADMIN_PASS=""
25
STANDARD_USER="fwd_user"
26
STANDARD_PASS=""
27

    
28
function show_usage()
29
{
30
   cat <<EOF
31
Usage: $0 [options]
32

    
33
Creates a PostgreSQL cluster with upstream initdb, compatible with the official
34
postgres Docker image. Safe to run on Ubuntu (host) or in an Ubuntu-based container.
35

    
36
Options:
37
  --pgdata DIR                 Data directory (default: $PGDATA_DEFAULT)
38
  --locale LOCALE              Cluster locale (default: $LOCALE_DEFAULT)
39
  --encoding ENC               Cluster encoding (default: $ENCODING_DEFAULT)
40
  --auth METHOD                pg_hba auth method: scram-sha-256 | md5 | password | trust
41
                               (default: $AUTH_DEFAULT)
42

    
43
  --postgres-password PASS     Initial password for postgres superuser
44
  --admin-user NAME            Admin role name (default: $ADMIN_USER)
45
  --admin-pass PASS            Admin role password
46
  --user-user NAME             Standard role name (default: $STANDARD_USER)
47
  --user-pass PASS             Standard role password
48

    
49
  --temp-port PORT             Temporary port for bootstrap (default: $TEMP_PORT_DEFAULT)
50
  --socket-dir DIR             Unix socket dir for bootstrap (default: $SOCKET_DIR_DEFAULT)
51

    
52
Examples:
53
  $0 --pgdata /data/pg17 --locale en_US.UTF-8 --encoding UTF8 \\
54
     --auth scram-sha-256 \\
55
     --postgres-password adminpw \\
56
     --admin-user app_admin --admin-pass S3cret! \\
57
     --user-user app_user --user-pass userpw
58

    
59
Notes:
60
- Uses initdb (NOT pg_createcluster), so the layout matches postgres Docker images.
61
- Ensures correct ownership and permissions on PGDATA.
62
EOF
63
}
64

    
65
# Parse args
66
while [[ $# -gt 0 ]]; do
67
   case "$1" in
68
      --pgdata)             PGDATA="$2"; shift 2 ;;
69
      --locale)             DB_LOCALE="$2"; shift 2 ;;
70
      --encoding)           DB_ENCODING="$2"; shift 2 ;;
71
      --auth)               AUTH_METHOD="$2"; shift 2 ;;
72
      --postgres-password)  POSTGRES_PASSWORD="$2"; shift 2 ;;
73
      --admin-user)         ADMIN_USER="$2"; shift 2 ;;
74
      --admin-pass)         ADMIN_PASS="$2"; shift 2 ;;
75
      --user-user)          STANDARD_USER="$2"; shift 2 ;;
76
      --user-pass)          STANDARD_PASS="$2"; shift 2 ;;
77
      --temp-port)          TEMP_PORT="$2"; shift 2 ;;
78
      --socket-dir)         SOCKET_DIR="$2"; shift 2 ;;
79
      -h|--help|-?)         show_usage; exit 0 ;;
80
      *) echo "ERROR: Unknown option: $1"; show_usage; exit 1 ;;
81
   esac
82
done
83

    
84
# ---- Helper: run command as the postgres system user when available ----
85
function run_as_postgres()
86
{
87
   if command -v gosu >/dev/null && [ "$(id -u)" = "0" ]; then
88
      gosu postgres "$@"
89
   elif [ "$(id -un)" = "postgres" ]; then
90
      "$@"
91
   else
92
      sudo -u postgres "$@"
93
   fi
94
}
95

    
96
# Build safe SQL using psql variables + format(%I,%L) to avoid injection
97
function create_roles_sql() {
98
  cat <<'SQL'
99
-- Create standard user if missing
100
SELECT format(
101
  'CREATE ROLE %I LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE',
102
  :'std_user', :'std_pass'
103
)
104
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = :'std_user')
105
\gexec
106

    
107
-- Create admin user if missing
108
SELECT format(
109
  'CREATE ROLE %I LOGIN PASSWORD %L SUPERUSER CREATEDB CREATEROLE',
110
  :'admin_user', :'admin_pass'
111
)
112
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = :'admin_user')
113
\gexec
114

    
115
-- Optionally set postgres password if provided
116
\if :{?pg_pass}
117
ALTER ROLE postgres WITH PASSWORD :'pg_pass';
118
\endif
119
SQL
120
}
121

    
122
# ---- Checks ----
123
function need()
124
{
125
   command -v "$1" >/dev/null 2>&1 || { echo "ERROR: Missing required command: $1" >&2; exit 1; }
126
}
127

    
128
# Setup full path, since sudo gets a PATH without PG
129
pg_path="/usr/lib/postgresql/${PG_VERSION}/bin"
130
need ${pg_path}/initdb
131
need ${pg_path}/pg_ctl
132
need ${pg_path}/psql
133

    
134
# ---- Prepare PGDATA (ownership and permissions) ----
135
sudo mkdir -p "$PGDATA"
136
# Ensure ownership by postgres user (UID/GID may be 999:999 inside official postgres image)
137
if id postgres >/dev/null 2>&1; then
138
   sudo chown -R postgres:postgres "$PGDATA" || true
139
fi
140
sudo chmod 700 "$PGDATA" || true
141

    
142
# ---- Initialize cluster if needed. Bail if it is there. ----
143
if [[ ! -s "$PGDATA/PG_VERSION" ]]; then
144
   echo "INFO: Initializing cluster in $PGDATA (locale=$DB_LOCALE, encoding=$DB_ENCODING)..."
145

    
146
   PWFILE=""
147
   if [[ -n "$POSTGRES_PASSWORD" ]]; then
148
      PWFILE="$(mktemp)"
149
      printf '%s' "$POSTGRES_PASSWORD" > "$PWFILE"
150
   fi
151

    
152
   initdb_args=( -D "$PGDATA" --locale="$DB_LOCALE" --encoding="$DB_ENCODING" )
153
   # Set bootstrap superuser name explicitly (default is 'postgres' anyway)
154
   initdb_args+=( -U postgres )
155
   if [[ -n "$PWFILE" ]]; then
156
      initdb_args+=( --pwfile="$PWFILE" )
157
   fi
158

    
159
   run_as_postgres ${pg_path}/initdb "${initdb_args[@]}"
160

    
161
   [[ -n "$PWFILE" ]] && rm -f "$PWFILE"
162
else
163
   echo "WARNING: Cluster already exists at $PGDATA; exiting."
164
   exit 1
165
fi
166

    
167
# ---- Configure password_encryption if SCRAM selected ----
168
POSTGRESQL_CONF="$PGDATA/postgresql.conf"
169
if [[ "$AUTH_METHOD" == "scram-sha-256" ]]; then
170
   if ! grep -q '^[# ]*password_encryption[[:space:]]*=' "$POSTGRESQL_CONF" 2>/dev/null; then
171
      echo "password_encryption = scram-sha-256" | run_as_postgres tee -a "$POSTGRESQL_CONF" > /dev/null
172
   else
173
      run_as_postgres sed -i -E "s|^[# ]*password_encryption[[:space:]]*=.*|password_encryption = scram-sha-256|g" "$POSTGRESQL_CONF"
174
   fi
175
fi
176

    
177
# Ensure server will accept TCP later (listen on all; comment/uncomment as needed)
178
if ! grep -q '^[# ]*listen_addresses[[:space:]]*=' "$POSTGRESQL_CONF" 2>/dev/null; then
179
   echo "listen_addresses = '*'" | run_as_postgres tee -a "$POSTGRESQL_CONF" > /dev/null
180
else
181
   run_as_postgres sed -i -E "s|^[# ]*listen_addresses[[:space:]]*=.*|listen_addresses = '*'|g" "$POSTGRESQL_CONF"
182
fi
183

    
184
# ---- Configure pg_hba.conf ----
185
PG_HBA="$PGDATA/pg_hba.conf"
186
echo "INFO: Configuring pg_hba.conf with auth method: $AUTH_METHOD"
187
# Keep any existing lines, but ensure we have our standard baseline at the end
188
run_as_postgres tee -a "$PG_HBA" > /dev/null <<HBA
189

    
190
# Added by create_pg_cluster.sh
191
# Local socket connections
192
local   all             all                                     $AUTH_METHOD
193
# IPv4 localhost
194
host    all             all             127.0.0.1/32            $AUTH_METHOD
195
# IPv6 localhost
196
host    all             all             ::1/128                 $AUTH_METHOD
197
# (Optional) allow all hosts - restrict in production as needed
198
# host   all             all             0.0.0.0/0               $AUTH_METHOD
199
# host   all             all             ::/0                    $AUTH_METHOD
200
HBA
201

    
202
# ---- Bootstrap: start server locally (socket only) to create roles ----
203
echo "INFO: Starting temporary server for role creation..."
204
run_as_postgres ${pg_path}/pg_ctl -D "$PGDATA" \
205
  -o "-c listen_addresses='' -c unix_socket_directories='$SOCKET_DIR' -p $TEMP_PORT" \
206
  -w start
207

    
208
# Setup 'set' variables
209
PSQL_ARGS=(
210
  --set=std_user="${STANDARD_USER}"
211
  --set=std_pass="${STANDARD_PASS:-}"
212
  --set=admin_user="${ADMIN_USER}"
213
  --set=admin_pass="${ADMIN_PASS:-}"
214
)
215
[[ -n "$POSTGRES_PASSWORD" ]] && PSQL_ARGS+=(--set=pg_pass="$POSTGRES_PASSWORD")
216

    
217
# Run SQL in create_roles_sql function
218
echo "INFO: Creating roles..."
219
create_roles_sql | run_as_postgres psql -h "$SOCKET_DIR" -p "$TEMP_PORT" -U postgres -d postgres \
220
                                       "${PSQL_ARGS[@]}" \
221
                                       -v ON_ERROR_STOP=1
222

    
223
# Stop temp server
224
echo "INFO: Stopping temporary server..."
225
run_as_postgres ${pg_path}/pg_ctl -D "$PGDATA" -w stop
226

    
227
# Final ownership/permissions setup
228
if id postgres >/dev/null 2>&1; then
229
   sudo chown -R postgres:postgres "$PGDATA" || true
230
fi
231
sudo chmod 700 "$PGDATA" || true
232

    
233
# Show some useful information
234
cat <<SUM
235

    
236
✅ Cluster is ready at: $PGDATA
237

    
238
Next steps:
239
- To run with the official postgres image:
240
    docker run -e POSTGRES_PASSWORD='${POSTGRES_PASSWORD:-<set>}' \\
241
               -v <volume_name>:"$PGDATA" \\
242
               -p 5432:5432 \\
243
               postgres:${PG_VERSION}
244

    
245
- Or start locally (outside Docker):
246
    as postgres:  ${pg_path}/pg_ctl -D "$PGDATA" -w start
247

    
248
Notes:
249
- listen_addresses='*' has been set. Adjust pg_hba.conf to restrict host access before exposing ports.
250
- Auth method in pg_hba.conf: $AUTH_METHOD
251
SUM