|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -Eeo pipefail |
| 3 | +# TODO swap to -Eeuo pipefail above (after handling all potentially-unset variables) |
| 4 | + |
| 5 | +# usage: file_env VAR [DEFAULT] |
| 6 | +# ie: file_env 'XYZ_DB_PASSWORD' 'example' |
| 7 | +# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of |
| 8 | +# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) |
| 9 | +file_env() { |
| 10 | + local var="$1" |
| 11 | + local fileVar="${var}_FILE" |
| 12 | + local def="${2:-}" |
| 13 | + if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then |
| 14 | + echo >&2 "error: both $var and $fileVar are set (but are exclusive)" |
| 15 | + exit 1 |
| 16 | + fi |
| 17 | + local val="$def" |
| 18 | + if [ "${!var:-}" ]; then |
| 19 | + val="${!var}" |
| 20 | + elif [ "${!fileVar:-}" ]; then |
| 21 | + val="$(< "${!fileVar}")" |
| 22 | + fi |
| 23 | + export "$var"="$val" |
| 24 | + unset "$fileVar" |
| 25 | +} |
| 26 | + |
| 27 | +# check to see if this file is being run or sourced from another script |
| 28 | +_is_sourced() { |
| 29 | + # https://unix.stackexchange.com/a/215279 |
| 30 | + [ "${#FUNCNAME[@]}" -ge 2 ] \ |
| 31 | + && [ "${FUNCNAME[0]}" = '_is_sourced' ] \ |
| 32 | + && [ "${FUNCNAME[1]}" = 'source' ] |
| 33 | +} |
| 34 | + |
| 35 | +# used to create initial postgres directories and if run as root, ensure ownership to the "postgres" user |
| 36 | +docker_create_db_directories() { |
| 37 | + local user; user="$(id -u)" |
| 38 | + |
| 39 | + mkdir -p "$PGDATA" |
| 40 | + # ignore failure since there are cases where we can't chmod (and PostgreSQL might fail later anyhow - it's picky about permissions of this directory) |
| 41 | + chmod 700 "$PGDATA" || : |
| 42 | + |
| 43 | + # ignore failure since it will be fine when using the image provided directory; see also https://github.com/docker-library/postgres/pull/289 |
| 44 | + mkdir -p /var/run/postgresql || : |
| 45 | + chmod 775 /var/run/postgresql || : |
| 46 | + |
| 47 | + # Create the transaction log directory before initdb is run so the directory is owned by the correct user |
| 48 | + if [ -n "$POSTGRES_INITDB_WALDIR" ]; then |
| 49 | + mkdir -p "$POSTGRES_INITDB_WALDIR" |
| 50 | + if [ "$user" = '0' ]; then |
| 51 | + find "$POSTGRES_INITDB_WALDIR" \! -user postgres -exec chown postgres '{}' + |
| 52 | + fi |
| 53 | + chmod 700 "$POSTGRES_INITDB_WALDIR" |
| 54 | + fi |
| 55 | + |
| 56 | + # allow the container to be started with `--user` |
| 57 | + if [ "$user" = '0' ]; then |
| 58 | + find "$PGDATA" \! -user postgres -exec chown postgres '{}' + |
| 59 | + find /var/run/postgresql \! -user postgres -exec chown postgres '{}' + |
| 60 | + fi |
| 61 | +} |
| 62 | + |
| 63 | +# initialize empty PGDATA directory with new database via 'initdb' |
| 64 | +# arguments to `initdb` can be passed via POSTGRES_INITDB_ARGS or as arguments to this function |
| 65 | +# `initdb` automatically creates the "postgres", "template0", and "template1" dbnames |
| 66 | +# this is also where the database user is created, specified by `POSTGRES_USER` env |
| 67 | +docker_init_database_dir() { |
| 68 | + # "initdb" is particular about the current user existing in "/etc/passwd", so we use "nss_wrapper" to fake that if necessary |
| 69 | + # see https://github.com/docker-library/postgres/pull/253, https://github.com/docker-library/postgres/issues/359, https://cwrap.org/nss_wrapper.html |
| 70 | + if ! getent passwd "$(id -u)" &> /dev/null && [ -e /usr/lib/libnss_wrapper.so ]; then |
| 71 | + export LD_PRELOAD='/usr/lib/libnss_wrapper.so' |
| 72 | + export NSS_WRAPPER_PASSWD="$(mktemp)" |
| 73 | + export NSS_WRAPPER_GROUP="$(mktemp)" |
| 74 | + echo "postgres:x:$(id -u):$(id -g):PostgreSQL:$PGDATA:/bin/false" > "$NSS_WRAPPER_PASSWD" |
| 75 | + echo "postgres:x:$(id -g):" > "$NSS_WRAPPER_GROUP" |
| 76 | + fi |
| 77 | + |
| 78 | + if [ -n "$POSTGRES_INITDB_WALDIR" ]; then |
| 79 | + set -- --waldir "$POSTGRES_INITDB_WALDIR" "$@" |
| 80 | + fi |
| 81 | + |
| 82 | + eval 'initdb --username="$POSTGRES_USER" --pwfile=<(echo "$POSTGRES_PASSWORD") '"$POSTGRES_INITDB_ARGS"' "$@"' |
| 83 | + |
| 84 | + # unset/cleanup "nss_wrapper" bits |
| 85 | + if [ "${LD_PRELOAD:-}" = '/usr/lib/libnss_wrapper.so' ]; then |
| 86 | + rm -f "$NSS_WRAPPER_PASSWD" "$NSS_WRAPPER_GROUP" |
| 87 | + unset LD_PRELOAD NSS_WRAPPER_PASSWD NSS_WRAPPER_GROUP |
| 88 | + fi |
| 89 | + |
| 90 | + echo "listen_addresses = '*'" >> "$PGDATA/postgresql.conf" |
| 91 | +} |
| 92 | + |
| 93 | +# print large warning if POSTGRES_PASSWORD is long |
| 94 | +# error if both POSTGRES_PASSWORD is empty and POSTGRES_HOST_AUTH_METHOD is not 'trust' |
| 95 | +# print large warning if POSTGRES_HOST_AUTH_METHOD is set to 'trust' |
| 96 | +# assumes database is not set up, ie: [ -z "$DATABASE_ALREADY_EXISTS" ] |
| 97 | +docker_verify_minimum_env() { |
| 98 | + # check password first so we can output the warning before postgres |
| 99 | + # messes it up |
| 100 | + if [ "${#POSTGRES_PASSWORD}" -ge 100 ]; then |
| 101 | + cat >&2 <<-'EOWARN' |
| 102 | +
|
| 103 | + WARNING: The supplied POSTGRES_PASSWORD is 100+ characters. |
| 104 | +
|
| 105 | + This will not work if used via PGPASSWORD with "psql". |
| 106 | +
|
| 107 | + https://www.postgresql.org/message-id/flat/E1Rqxp2-0004Qt-PL%40wrigleys.postgresql.org (BUG #6412) |
| 108 | + https://github.com/docker-library/postgres/issues/507 |
| 109 | +
|
| 110 | + EOWARN |
| 111 | + fi |
| 112 | + if [ -z "$POSTGRES_PASSWORD" ] && [ 'trust' != "$POSTGRES_HOST_AUTH_METHOD" ]; then |
| 113 | + # The - option suppresses leading tabs but *not* spaces. :) |
| 114 | + cat >&2 <<-'EOE' |
| 115 | + Error: Database is uninitialized and superuser password is not specified. |
| 116 | + You must specify POSTGRES_PASSWORD to a non-empty value for the |
| 117 | + superuser. For example, "-e POSTGRES_PASSWORD=password" on "docker run". |
| 118 | +
|
| 119 | + You may also use "POSTGRES_HOST_AUTH_METHOD=trust" to allow all |
| 120 | + connections without a password. This is *not* recommended. |
| 121 | +
|
| 122 | + See PostgreSQL documentation about "trust": |
| 123 | + https://www.postgresql.org/docs/current/auth-trust.html |
| 124 | + EOE |
| 125 | + exit 1 |
| 126 | + fi |
| 127 | + if [ 'trust' = "$POSTGRES_HOST_AUTH_METHOD" ]; then |
| 128 | + cat >&2 <<-'EOWARN' |
| 129 | + ******************************************************************************** |
| 130 | + WARNING: POSTGRES_HOST_AUTH_METHOD has been set to "trust". This will allow |
| 131 | + anyone with access to the Postgres port to access your database without |
| 132 | + a password, even if POSTGRES_PASSWORD is set. See PostgreSQL |
| 133 | + documentation about "trust": |
| 134 | + https://www.postgresql.org/docs/current/auth-trust.html |
| 135 | + In Docker's default configuration, this is effectively any other |
| 136 | + container on the same system. |
| 137 | +
|
| 138 | + It is not recommended to use POSTGRES_HOST_AUTH_METHOD=trust. Replace |
| 139 | + it with "-e POSTGRES_PASSWORD=password" instead to set a password in |
| 140 | + "docker run". |
| 141 | + ******************************************************************************** |
| 142 | + EOWARN |
| 143 | + fi |
| 144 | +} |
| 145 | + |
| 146 | +# usage: docker_process_init_files [file [file [...]]] |
| 147 | +# ie: docker_process_init_files /always-initdb.d/* |
| 148 | +# process initializer files, based on file extensions and permissions |
| 149 | +docker_process_init_files() { |
| 150 | + # psql here for backwards compatibility "${psql[@]}" |
| 151 | + psql=( docker_process_sql ) |
| 152 | + |
| 153 | + echo |
| 154 | + local f |
| 155 | + for f; do |
| 156 | + case "$f" in |
| 157 | + *.sh) |
| 158 | + # https://github.com/docker-library/postgres/issues/450#issuecomment-393167936 |
| 159 | + # https://github.com/docker-library/postgres/pull/452 |
| 160 | + if [ -x "$f" ]; then |
| 161 | + echo "$0: running $f" |
| 162 | + "$f" |
| 163 | + else |
| 164 | + echo "$0: sourcing $f" |
| 165 | + . "$f" |
| 166 | + fi |
| 167 | + ;; |
| 168 | + *.sql) echo "$0: running $f"; docker_process_sql -f "$f"; echo ;; |
| 169 | + *.sql.gz) echo "$0: running $f"; gunzip -c "$f" | docker_process_sql; echo ;; |
| 170 | + *.sql.xz) echo "$0: running $f"; xzcat "$f" | docker_process_sql; echo ;; |
| 171 | + *) echo "$0: ignoring $f" ;; |
| 172 | + esac |
| 173 | + echo |
| 174 | + done |
| 175 | +} |
| 176 | + |
| 177 | +# Execute sql script, passed via stdin (or -f flag of pqsl) |
| 178 | +# usage: docker_process_sql [psql-cli-args] |
| 179 | +# ie: docker_process_sql --dbname=mydb <<<'INSERT ...' |
| 180 | +# ie: docker_process_sql -f my-file.sql |
| 181 | +# ie: docker_process_sql <my-file.sql |
| 182 | +docker_process_sql() { |
| 183 | + local query_runner=( psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --no-password ) |
| 184 | + if [ -n "$POSTGRES_DB" ]; then |
| 185 | + query_runner+=( --dbname "$POSTGRES_DB" ) |
| 186 | + fi |
| 187 | + |
| 188 | + PGHOST= PGHOSTADDR= "${query_runner[@]}" "$@" |
| 189 | +} |
| 190 | + |
| 191 | +# create initial database |
| 192 | +# uses environment variables for input: POSTGRES_DB |
| 193 | +docker_setup_db() { |
| 194 | + local dbAlreadyExists |
| 195 | + dbAlreadyExists="$( |
| 196 | + POSTGRES_DB= docker_process_sql --dbname postgres --set db="$POSTGRES_DB" --tuples-only <<-'EOSQL' |
| 197 | + SELECT 1 FROM pg_database WHERE datname = :'db' ; |
| 198 | + EOSQL |
| 199 | + )" |
| 200 | + if [ -z "$dbAlreadyExists" ]; then |
| 201 | + POSTGRES_DB= docker_process_sql --dbname postgres --set db="$POSTGRES_DB" <<-'EOSQL' |
| 202 | + CREATE DATABASE :"db" ; |
| 203 | + EOSQL |
| 204 | + echo |
| 205 | + fi |
| 206 | +} |
| 207 | + |
| 208 | +# Loads various settings that are used elsewhere in the script |
| 209 | +# This should be called before any other functions |
| 210 | +docker_setup_env() { |
| 211 | + file_env 'POSTGRES_PASSWORD' |
| 212 | + |
| 213 | + file_env 'POSTGRES_USER' 'postgres' |
| 214 | + file_env 'POSTGRES_DB' "$POSTGRES_USER" |
| 215 | + file_env 'POSTGRES_INITDB_ARGS' |
| 216 | + # default authentication method is md5 |
| 217 | + : "${POSTGRES_HOST_AUTH_METHOD:=md5}" |
| 218 | + |
| 219 | + declare -g DATABASE_ALREADY_EXISTS |
| 220 | + # look specifically for PG_VERSION, as it is expected in the DB dir |
| 221 | + if [ -s "$PGDATA/PG_VERSION" ]; then |
| 222 | + DATABASE_ALREADY_EXISTS='true' |
| 223 | + fi |
| 224 | +} |
| 225 | + |
| 226 | +# append POSTGRES_HOST_AUTH_METHOD to pg_hba.conf for "host" connections |
| 227 | +pg_setup_hba_conf() { |
| 228 | + { |
| 229 | + echo |
| 230 | + if [ 'trust' = "$POSTGRES_HOST_AUTH_METHOD" ]; then |
| 231 | + echo '# warning trust is enabled for all connections' |
| 232 | + echo '# see https://www.postgresql.org/docs/12/auth-trust.html' |
| 233 | + fi |
| 234 | + echo "host all all all $POSTGRES_HOST_AUTH_METHOD" |
| 235 | + } >> "$PGDATA/pg_hba.conf" |
| 236 | +} |
| 237 | + |
| 238 | +# start socket-only postgresql server for setting up or running scripts |
| 239 | +# all arguments will be passed along as arguments to `postgres` (via pg_ctl) |
| 240 | +docker_temp_server_start() { |
| 241 | + if [ "$1" = 'postgres' ]; then |
| 242 | + shift |
| 243 | + fi |
| 244 | + |
| 245 | + # internal start of server in order to allow setup using psql client |
| 246 | + # does not listen on external TCP/IP and waits until start finishes |
| 247 | + set -- "$@" -c listen_addresses='' -p "${PGPORT:-5432}" |
| 248 | + |
| 249 | + PGUSER="${PGUSER:-$POSTGRES_USER}" \ |
| 250 | + pg_ctl -D "$PGDATA" \ |
| 251 | + -o "$(printf '%q ' "$@")" \ |
| 252 | + -w start |
| 253 | +} |
| 254 | + |
| 255 | +# stop postgresql server after done setting up user and running scripts |
| 256 | +docker_temp_server_stop() { |
| 257 | + PGUSER="${PGUSER:-postgres}" \ |
| 258 | + pg_ctl -D "$PGDATA" -m fast -w stop |
| 259 | +} |
| 260 | + |
| 261 | +# check arguments for an option that would cause postgres to stop |
| 262 | +# return true if there is one |
| 263 | +_pg_want_help() { |
| 264 | + local arg |
| 265 | + for arg; do |
| 266 | + case "$arg" in |
| 267 | + # postgres --help | grep 'then exit' |
| 268 | + # leaving out -C on purpose since it always fails and is unhelpful: |
| 269 | + # postgres: could not access the server configuration file "/var/lib/postgresql/data/postgresql.conf": No such file or directory |
| 270 | + -'?'|--help|--describe-config|-V|--version) |
| 271 | + return 0 |
| 272 | + ;; |
| 273 | + esac |
| 274 | + done |
| 275 | + return 1 |
| 276 | +} |
| 277 | + |
| 278 | +_main() { |
| 279 | + # if first arg looks like a flag, assume we want to run postgres server |
| 280 | + if [ "${1:0:1}" = '-' ]; then |
| 281 | + set -- postgres "$@" |
| 282 | + fi |
| 283 | + |
| 284 | + if [ "$1" = 'postgres' ] && ! _pg_want_help "$@"; then |
| 285 | + docker_setup_env |
| 286 | + # setup data directories and permissions (when run as root) |
| 287 | + docker_create_db_directories |
| 288 | + if [ "$(id -u)" = '0' ]; then |
| 289 | + # then restart script as postgres user |
| 290 | + exec gosu postgres "$BASH_SOURCE" "$@" |
| 291 | + fi |
| 292 | + |
| 293 | + # only run initialization on an empty data directory |
| 294 | + if [ -z "$DATABASE_ALREADY_EXISTS" ]; then |
| 295 | + docker_verify_minimum_env |
| 296 | + |
| 297 | + # check dir permissions to reduce likelihood of half-initialized database |
| 298 | + ls /docker-entrypoint-initdb.d/ > /dev/null |
| 299 | + |
| 300 | + docker_init_database_dir |
| 301 | + pg_setup_hba_conf |
| 302 | + |
| 303 | + # PGPASSWORD is required for psql when authentication is required for 'local' connections via pg_hba.conf and is otherwise harmless |
| 304 | + # e.g. when '--auth=md5' or '--auth-local=md5' is used in POSTGRES_INITDB_ARGS |
| 305 | + export PGPASSWORD="${PGPASSWORD:-$POSTGRES_PASSWORD}" |
| 306 | + docker_temp_server_start "$@" |
| 307 | + |
| 308 | + docker_setup_db |
| 309 | + docker_process_init_files /docker-entrypoint-initdb.d/* |
| 310 | + |
| 311 | + docker_temp_server_stop |
| 312 | + unset PGPASSWORD |
| 313 | + |
| 314 | + echo |
| 315 | + echo 'PostgreSQL init process complete; ready for start up.' |
| 316 | + echo |
| 317 | + else |
| 318 | + echo |
| 319 | + echo 'PostgreSQL Database directory appears to contain a database; Skipping initialization' |
| 320 | + echo |
| 321 | + fi |
| 322 | + fi |
| 323 | + |
| 324 | + exec "$@" |
| 325 | +} |
| 326 | + |
| 327 | +if ! _is_sourced; then |
| 328 | + _main "$@" |
| 329 | +fi |
0 commit comments