mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2026-10-30 16:20:04 -05:00
Reformat pkg/docker/entrypoint.sh to use bash conditionals, arithmetic and parameter expansion instead of the workarounds needed for BusyBox ash. The email validation settings and PGADMIN_DEFAULT_EMAIL are now read from the environment inside Python rather than interpolated into the Python source, so an address containing an apostrophe works, and a Python error during validation stops the container instead of passing silently.
354 lines
14 KiB
Bash
Executable File
354 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
########################################################################
|
|
# PUID/PGID support
|
|
#
|
|
# When the container runs as root (e.g. --user root), the pgadmin user
|
|
# is reassigned to the requested UID/GID and all initialization +
|
|
# gunicorn run via su-exec as that user.
|
|
#
|
|
# When the container runs as non-root (default USER 5050, or OpenShift
|
|
# random UID), PUID/PGID are ignored and everything runs as the
|
|
# current user.
|
|
########################################################################
|
|
|
|
PUID=${PUID:-5050}
|
|
PGID=${PGID:-0}
|
|
|
|
# Validate PUID/PGID are numeric and in acceptable range
|
|
if [[ ! $PUID =~ ^[0-9]+$ ]]; then
|
|
echo "ERROR: PUID must be a numeric value, got '$PUID'"
|
|
exit 1
|
|
fi
|
|
if [[ ! $PGID =~ ^[0-9]+$ ]]; then
|
|
echo "ERROR: PGID must be a numeric value, got '$PGID'"
|
|
exit 1
|
|
fi
|
|
if (( 10#$PUID == 0 )); then
|
|
echo "ERROR: PUID=0 (root) is not allowed. Use a non-root UID."
|
|
exit 1
|
|
fi
|
|
|
|
if (( EUID == 0 )); then
|
|
# Ensure a group with the target GID exists
|
|
if ! getent group "$PGID" &>/dev/null && ! addgroup -g "$PGID" pggroup; then
|
|
echo "ERROR: Failed to create group with GID=$PGID"
|
|
exit 1
|
|
fi
|
|
|
|
# Reassign the pgadmin user to the desired UID/GID
|
|
if ! usermod -o -u "$PUID" -g "$PGID" pgadmin; then
|
|
echo "ERROR: Failed to set pgadmin user to UID=$PUID GID=$PGID"
|
|
exit 1
|
|
fi
|
|
|
|
# Fix ownership of runtime directories BEFORE any initialization
|
|
for dir in /run/pgadmin /var/lib/pgadmin; do
|
|
if [[ -d $dir ]]; then
|
|
chown -R "$PUID:$PGID" "$dir"
|
|
fi
|
|
done
|
|
|
|
# Fix ownership of individual files (no -R needed)
|
|
if [[ -e /pgadmin4/config_distro.py ]]; then
|
|
chown "$PUID:$PGID" /pgadmin4/config_distro.py
|
|
fi
|
|
|
|
if [[ -d /certs ]]; then
|
|
chown -R "$PUID:$PGID" /certs
|
|
fi
|
|
|
|
SU_EXEC="su-exec $PUID:$PGID"
|
|
echo "pgAdmin will run as UID=$PUID, GID=$PGID"
|
|
else
|
|
SU_EXEC=""
|
|
|
|
# Fixup the passwd file, in case we're on OpenShift
|
|
if ! whoami &>/dev/null && (( EUID != 5050 )) && [[ -w /etc/passwd ]]; then
|
|
echo "${USER_NAME:-pgadminr}:x:${EUID}:0:${USER_NAME:-pgadminr} user:${HOME}:/sbin/nologin" >>/etc/passwd
|
|
fi
|
|
fi
|
|
|
|
# Decide which python interpreter to use for gunicorn.
|
|
#
|
|
# /venv/bin/python3-cap is a symlink to /usr/local/bin/python3-cap, a copy
|
|
# of the system python carrying CAP_NET_BIND_SERVICE. It is needed to bind
|
|
# to privileged ports (the default 80/443) as the non-root pgadmin user.
|
|
#
|
|
# Some platforms refuse to honor file capabilities, in which case execing
|
|
# the capped binary either fails outright or silently strips the caps so
|
|
# bind() to a port <1024 still returns EPERM:
|
|
#
|
|
# - NoNewPrivs=1 (--security-opt=no-new-privileges, OpenShift's
|
|
# allowPrivilegeEscalation: false): the kernel silently strips file
|
|
# capabilities on exec.
|
|
# - CAP_NET_BIND_SERVICE missing from the bounding set (--cap-drop=ALL,
|
|
# OpenShift restricted-v2 SCC): exec of the capped binary returns
|
|
# EPERM.
|
|
#
|
|
# Detect either condition via /proc/self/status. When restricted, fall
|
|
# back to the un-capped venv python and (if the user has not picked a
|
|
# port) default PGADMIN_LISTEN_PORT to 8080/8443 so the server can
|
|
# actually bind.
|
|
PYTHON_BIN=/venv/bin/python3-cap
|
|
restricted=0
|
|
|
|
if grep -q '^NoNewPrivs:[[:space:]]*1' /proc/self/status 2>/dev/null; then
|
|
restricted=1
|
|
fi
|
|
|
|
if (( restricted == 0 )); then
|
|
cap_bnd=$(awk '/^CapBnd:/ { print $2 }' /proc/self/status 2>/dev/null)
|
|
if [[ -n $cap_bnd ]] && (( (0x$cap_bnd & 0x400) == 0 )); then
|
|
restricted=1
|
|
fi
|
|
fi
|
|
|
|
if (( restricted == 1 )) || [[ ! -x /venv/bin/python3-cap ]]; then
|
|
PYTHON_BIN=/venv/bin/python3
|
|
if [[ -z $PGADMIN_LISTEN_PORT ]]; then
|
|
if [[ -n $PGADMIN_ENABLE_TLS ]]; then
|
|
export PGADMIN_LISTEN_PORT=8443
|
|
else
|
|
export PGADMIN_LISTEN_PORT=8080
|
|
fi
|
|
echo "Restricted security context detected; defaulting PGADMIN_LISTEN_PORT to $PGADMIN_LISTEN_PORT."
|
|
fi
|
|
fi
|
|
|
|
# usage: file_env VAR [DEFAULT] ie: file_env 'XYZ_DB_PASSWORD' 'example'
|
|
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
|
|
# "$XYZ_DB_PASSWORD" from a file, for Docker's secrets feature)
|
|
function file_env() {
|
|
local var="$1"
|
|
local fileVar="${var}_FILE"
|
|
local def="${2:-}"
|
|
if [[ -n ${!var:-} && -n ${!fileVar:-} ]]; then
|
|
printf >&2 'error: both %s and %s are set (but are exclusive)\n' "$var" "$fileVar"
|
|
exit 1
|
|
fi
|
|
local val="$def"
|
|
if [[ -n ${!var:-} ]]; then
|
|
val="${!var}"
|
|
elif [[ -n ${!fileVar:-} && ! -r ${!fileVar} ]]; then
|
|
printf >&2 'error: %s is set to "%s" but the file does not exist or is not readable\n' \
|
|
"$fileVar" "${!fileVar}"
|
|
exit 1
|
|
elif [[ -n ${!fileVar:-} ]]; then
|
|
val="$(<"${!fileVar}")"
|
|
fi
|
|
export "$var"="$val"
|
|
unset "$fileVar"
|
|
}
|
|
|
|
# Set values for config variables that can be passed using secrets
|
|
if [[ -n $PGADMIN_CONFIG_CONFIG_DATABASE_URI_FILE ]]; then
|
|
file_env PGADMIN_CONFIG_CONFIG_DATABASE_URI
|
|
fi
|
|
file_env PGADMIN_DEFAULT_PASSWORD
|
|
|
|
# TO enable custom path for config_distro, pass config distro path via environment variable.
|
|
export CONFIG_DISTRO_FILE_PATH="${PGADMIN_CUSTOM_CONFIG_DISTRO_FILE:-/pgadmin4/config_distro.py}"
|
|
# Populate config_distro.py. This has some default config, as well as anything
|
|
# provided by the user through the PGADMIN_CONFIG_* environment variables.
|
|
# Only update the file on first launch. The empty file is created only in default path during the
|
|
# container build so it can have the required ownership.
|
|
if [[ ! -e $CONFIG_DISTRO_FILE_PATH || "$(wc -m "$CONFIG_DISTRO_FILE_PATH" 2>/dev/null | awk '{ print $1 }')" == 0 ]]; then
|
|
cat <<EOF >"$CONFIG_DISTRO_FILE_PATH"
|
|
CA_FILE = '/etc/ssl/certs/ca-certificates.crt'
|
|
LOG_FILE = '/dev/null'
|
|
HELP_PATH = '../../docs'
|
|
DEFAULT_BINARY_PATHS = {
|
|
'pg': '/usr/local/pgsql-18',
|
|
'pg-18': '/usr/local/pgsql-18',
|
|
'pg-17': '/usr/local/pgsql-17',
|
|
'pg-16': '/usr/local/pgsql-16',
|
|
'pg-15': '/usr/local/pgsql-15',
|
|
'pg-14': '/usr/local/pgsql-14',
|
|
'pg-13': '/usr/local/pgsql-13'
|
|
}
|
|
EOF
|
|
|
|
# Iterate over PGADMIN_CONFIG_* environment variables and writes them to the config file
|
|
for var in "${!PGADMIN_CONFIG_@}"; do
|
|
# Get the raw value
|
|
val="${!var}"
|
|
# This normalization step is what makes 'true', 'True'
|
|
if [[ "${val,,}" =~ ^(true|false)$ ]]; then
|
|
val="${val,,}"
|
|
val="${val^}"
|
|
fi
|
|
echo "${var#PGADMIN_CONFIG_} = $val" >>"$CONFIG_DISTRO_FILE_PATH"
|
|
done
|
|
|
|
# If running as root with custom config distro path, fix ownership
|
|
if (( EUID == 0 )) && [[ $CONFIG_DISTRO_FILE_PATH != "/pgadmin4/config_distro.py" ]]; then
|
|
chown "$PUID:$PGID" "$CONFIG_DISTRO_FILE_PATH"
|
|
fi
|
|
fi
|
|
|
|
# Check whether the external configuration database exists if it is being used.
|
|
#
|
|
# The URI is read inside Python via os.environ — the shell no longer
|
|
# participates in Python-literal quoting. `ast.literal_eval` unwraps the
|
|
# legacy `'url'` form that the config_distro.py generation convention
|
|
# requires; raw values pass through unchanged. On any Python failure the
|
|
# "False" default below is preserved so first-launch user setup still
|
|
# runs (see #9984).
|
|
external_config_db_exists="False"
|
|
if [[ -n $PGADMIN_CONFIG_CONFIG_DATABASE_URI ]]; then
|
|
result=$(cd /pgadmin4/pgadmin/utils && $SU_EXEC /venv/bin/python3 -c "
|
|
import os, ast
|
|
from check_external_config_db import check_external_config_db
|
|
raw = os.environ['PGADMIN_CONFIG_CONFIG_DATABASE_URI']
|
|
try:
|
|
uri = ast.literal_eval(raw)
|
|
except (ValueError, SyntaxError):
|
|
uri = raw
|
|
print(check_external_config_db(uri))
|
|
" 2>/dev/null)
|
|
if [[ -n $result ]]; then
|
|
external_config_db_exists="$result"
|
|
fi
|
|
fi
|
|
|
|
# DRY of the code to load the PGADMIN_SERVER_JSON_FILE
|
|
function load_server_json_file() {
|
|
export PGADMIN_SERVER_JSON_FILE="${PGADMIN_SERVER_JSON_FILE:-/pgadmin4/servers.json}"
|
|
|
|
EXTRA_ARGS=""
|
|
|
|
if [[ $PGADMIN_REPLACE_SERVERS_ON_STARTUP == "True" ]]; then
|
|
EXTRA_ARGS="--replace"
|
|
fi
|
|
|
|
if [[ -f $PGADMIN_SERVER_JSON_FILE ]]; then
|
|
# When running in Desktop mode, no user is created
|
|
# so we have to import servers anonymously
|
|
if [[ $PGADMIN_CONFIG_SERVER_MODE == "False" ]]; then
|
|
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py load-servers "$PGADMIN_SERVER_JSON_FILE" $EXTRA_ARGS
|
|
else
|
|
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py load-servers "$PGADMIN_SERVER_JSON_FILE" --user "$PGADMIN_DEFAULT_EMAIL" $EXTRA_ARGS
|
|
fi
|
|
fi
|
|
}
|
|
|
|
if [[ ! -f /var/lib/pgadmin/pgadmin4.db && $external_config_db_exists == "False" ]]; then
|
|
if [[ -z $PGADMIN_DEFAULT_EMAIL || (-z $PGADMIN_DEFAULT_PASSWORD && -z $PGADMIN_DEFAULT_PASSWORD_FILE) ]]; then
|
|
echo 'You need to define the PGADMIN_DEFAULT_EMAIL and PGADMIN_DEFAULT_PASSWORD or PGADMIN_DEFAULT_PASSWORD_FILE environment variables.'
|
|
exit 1
|
|
fi
|
|
|
|
# Validate PGADMIN_DEFAULT_EMAIL
|
|
#
|
|
# The settings and the email address are read inside Python via
|
|
# os.environ rather than interpolated into the Python source. Boolean
|
|
# settings are normalised in the same way as config_distro.py above, so
|
|
# 'true' and 'false' in any case are accepted.
|
|
is_valid_email=$(cd /pgadmin4/pgadmin/utils && $SU_EXEC /venv/bin/python3 -c "
|
|
import ast, os, sys
|
|
from validation_utils import validate_email
|
|
|
|
|
|
def setting(name, default):
|
|
raw = os.environ.get('PGADMIN_CONFIG_' + name, '')
|
|
if not raw:
|
|
return default
|
|
if raw.lower() in ('true', 'false'):
|
|
return raw.lower() == 'true'
|
|
try:
|
|
return ast.literal_eval(raw)
|
|
except (ValueError, SyntaxError):
|
|
return raw
|
|
|
|
|
|
email_config = {
|
|
'CHECK_EMAIL_DELIVERABILITY': setting('CHECK_EMAIL_DELIVERABILITY', False),
|
|
'ALLOW_SPECIAL_EMAIL_DOMAINS': setting('ALLOW_SPECIAL_EMAIL_DOMAINS', []),
|
|
'GLOBALLY_DELIVERABLE': setting('GLOBALLY_DELIVERABLE', True),
|
|
}
|
|
print(f'email config is {email_config}', file=sys.stderr)
|
|
print(validate_email(os.environ['PGADMIN_DEFAULT_EMAIL'], email_config))
|
|
") || exit 1
|
|
if [[ $is_valid_email == *False* ]]; then
|
|
echo "'$PGADMIN_DEFAULT_EMAIL' does not appear to be a valid email address. Please reset the PGADMIN_DEFAULT_EMAIL environment variable and try again."
|
|
echo "Validation output: $is_valid_email"
|
|
exit 1
|
|
fi
|
|
# Switch back to root directory for further process
|
|
cd /pgadmin4
|
|
|
|
# Set the default username and password in a
|
|
# backwards compatible way
|
|
export PGADMIN_SETUP_EMAIL="$PGADMIN_DEFAULT_EMAIL"
|
|
export PGADMIN_SETUP_PASSWORD="$PGADMIN_DEFAULT_PASSWORD"
|
|
|
|
# Initialize DB before starting Gunicorn
|
|
# Importing pgadmin4 (from this script) is enough
|
|
$SU_EXEC /venv/bin/python3 run_pgadmin.py
|
|
|
|
export PGADMIN_PREFERENCES_JSON_FILE="${PGADMIN_PREFERENCES_JSON_FILE:-/pgadmin4/preferences.json}"
|
|
|
|
# Pre-load any required servers
|
|
load_server_json_file
|
|
|
|
# Pre-load any required preferences
|
|
if [[ -f $PGADMIN_PREFERENCES_JSON_FILE ]]; then
|
|
if [[ $PGADMIN_CONFIG_SERVER_MODE == "False" ]]; then
|
|
DESKTOP_USER=$(cd /pgadmin4 && $SU_EXEC /venv/bin/python3 -c 'import config; print(config.DESKTOP_USER)')
|
|
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py set-prefs "$DESKTOP_USER" --input-file "$PGADMIN_PREFERENCES_JSON_FILE"
|
|
else
|
|
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py set-prefs "$PGADMIN_DEFAULT_EMAIL" --input-file "$PGADMIN_PREFERENCES_JSON_FILE"
|
|
fi
|
|
fi
|
|
# Copy the pgpass file passed using secrets
|
|
if [[ -n $PGPASS_FILE && -f $PGPASS_FILE ]]; then
|
|
if [[ $PGADMIN_CONFIG_SERVER_MODE == "False" ]]; then
|
|
cp "$PGPASS_FILE" /var/lib/pgadmin/.pgpass
|
|
chmod 600 /var/lib/pgadmin/.pgpass
|
|
# Fix ownership when running as root
|
|
if (( EUID == 0 )); then
|
|
chown "$PUID:$PGID" /var/lib/pgadmin/.pgpass
|
|
fi
|
|
else
|
|
PGADMIN_USER_CONFIG_DIR="${PGADMIN_DEFAULT_EMAIL//@/_}"
|
|
mkdir -p "/var/lib/pgadmin/storage/$PGADMIN_USER_CONFIG_DIR"
|
|
cp "$PGPASS_FILE" "/var/lib/pgadmin/storage/$PGADMIN_USER_CONFIG_DIR/.pgpass"
|
|
chmod 600 "/var/lib/pgadmin/storage/$PGADMIN_USER_CONFIG_DIR/.pgpass"
|
|
# Fix ownership when running as root
|
|
if (( EUID == 0 )); then
|
|
chown -R "$PUID:$PGID" "/var/lib/pgadmin/storage/$PGADMIN_USER_CONFIG_DIR"
|
|
fi
|
|
fi
|
|
fi
|
|
# If already initialised and PGADMIN_REPLACE_SERVERS_ON_STARTUP is set to true, then load the server json file.
|
|
elif [[ $PGADMIN_REPLACE_SERVERS_ON_STARTUP == "True" ]]; then
|
|
load_server_json_file
|
|
fi
|
|
|
|
# Start Postfix to handle password resets etc.
|
|
if [[ -z $PGADMIN_DISABLE_POSTFIX ]]; then
|
|
sudo /usr/sbin/postfix start
|
|
fi
|
|
|
|
# Get the session timeout from the pgAdmin config. We'll use this (in seconds)
|
|
# to define the Gunicorn worker timeout
|
|
TIMEOUT=$(cd /pgadmin4 && $SU_EXEC /venv/bin/python3 -c 'import config; print(config.SESSION_EXPIRATION_TIME * 60 * 60 * 24)')
|
|
|
|
# NOTE: currently pgadmin can run only with 1 worker due to sessions implementation
|
|
# Using --threads to have multi-threaded single-process worker
|
|
|
|
if [[ -n $PGADMIN_ENABLE_SOCK ]]; then
|
|
BIND_ADDRESS="unix:/run/pgadmin/pgadmin.sock"
|
|
elif [[ -n $PGADMIN_ENABLE_TLS ]]; then
|
|
BIND_ADDRESS="${PGADMIN_LISTEN_ADDRESS:-[::]}:${PGADMIN_LISTEN_PORT:-443}"
|
|
else
|
|
BIND_ADDRESS="${PGADMIN_LISTEN_ADDRESS:-[::]}:${PGADMIN_LISTEN_PORT:-80}"
|
|
fi
|
|
|
|
if [[ -n $PGADMIN_ENABLE_TLS ]]; then
|
|
exec $SU_EXEC "$PYTHON_BIN" /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --timeout "$TIMEOUT" --bind "$BIND_ADDRESS" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" --keyfile /certs/server.key --certfile /certs/server.cert -c gunicorn_config.py run_pgadmin:app
|
|
else
|
|
exec $SU_EXEC "$PYTHON_BIN" /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --limit-request-fields "${GUNICORN_LIMIT_REQUEST_FIELDS:-100}" --limit-request-field_size "${GUNICORN_LIMIT_REQUEST_FIELD_SIZE:-8190}" --timeout "$TIMEOUT" --bind "$BIND_ADDRESS" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" -c gunicorn_config.py run_pgadmin:app
|
|
fi
|