Skip to content

Commit

Permalink
Support multiple DATABASE_URLs (#109)
Browse files Browse the repository at this point in the history
* Entrypoint: move some variable definitions & reuse where possible

* Entrypoint: tiny formatting & consistency tweaks

* Entrypoint: refactor some parts into functions (needed for later)

* Entrypoint: support `DATABASE_URLS` with comma separated list of urls

* Add readme entry mentioning `DATABASE_URLS`

* minor cleanup

---------

Co-authored-by: Jeff Lambert <[email protected]>
  • Loading branch information
djbe and jflambert authored Feb 28, 2025
1 parent 36ac8ff commit cea2cf8
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 37 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,22 @@ docker run --rm \
edoburu/pgbouncer
```

Connecting should work as expected:
During startup we'll generate the necessary `userlist.txt` & `pgbouncer.ini` entries to match this information. Connecting should work as expected:

```sh
psql 'postgresql://user:pass@localhost/dbname'
```

> [!NOTE]
> If you need quick multi-user setup you can use `DATABASE_URLS` (instead of `DATABASE_URL`) with a comma (`,`) separated list of URLs.
>
> ```sh
> docker run --rm \
> -e DATABASE_URLS="postgres://foo:123@postgres-host/foo,postgres://bar:456@postgres-host/bar" \
> -p 5432:5432 \
> edoburu/pgbouncer
> ```
Configuration
-------------
Expand Down
112 changes: 76 additions & 36 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,75 +7,115 @@ set -e
# https://pgbouncer.github.io/config.html

PG_CONFIG_DIR=/etc/pgbouncer
PG_CONFIG_FILE="${PG_CONFIG_DIR}/pgbouncer.ini"
_AUTH_FILE="${AUTH_FILE:-$PG_CONFIG_DIR/userlist.txt}"

# Workaround userlist.txt missing issue
# https://github.com/edoburu/docker-pgbouncer/issues/33
if [ ! -e "${_AUTH_FILE}" ]; then
touch "${_AUTH_FILE}"
fi

if [ -n "$DATABASE_URL" ]; then
# Extract all info from a given URL. Sets variables because shell functions can't return multiple values.
#
# Parameters:
# - The url we should parse
# Returns (sets variables): DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME
function parse_url() {
# Thanks to https://stackoverflow.com/a/17287984/146289

# Allow to pass values like dj-database-url / django-environ accept
proto="$(echo $DATABASE_URL | grep :// | sed -e's,^\(.*://\).*,\1,g')"
url="$(echo $DATABASE_URL | sed -e s,$proto,,g)"
proto="$(echo $1 | grep :// | sed -e's,^\(.*://\).*,\1,g')"
url="$(echo $1 | sed -e s,$proto,,g)"

# extract the user and password (if any)
userpass=$(echo $url | grep @ | sed -r 's/^(.*)@([^@]*)$/\1/')
userpass="$(echo $url | grep @ | sed -r 's/^(.*)@([^@]*)$/\1/')"
DB_PASSWORD="$(echo $userpass | grep : | cut -d: -f2)"
if [ -n "$DB_PASSWORD" ]; then
DB_USER=$(echo $userpass | grep : | cut -d: -f1)
if [ -n "${DB_PASSWORD}" ]; then
DB_USER="$(echo $userpass | grep : | cut -d: -f1)"
else
DB_USER=$userpass
DB_USER="${userpass}"
fi

# extract the host -- updated
hostport=`echo $url | sed -e s,$userpass@,,g | cut -d/ -f1`
port=`echo $hostport | grep : | cut -d: -f2`
if [ -n "$port" ]; then
DB_HOST=`echo $hostport | grep : | cut -d: -f1`
DB_PORT=$port
DB_PORT="${port}"
else
DB_HOST=$hostport
DB_HOST="${hostport}"
fi

DB_NAME="$(echo $url | grep / | cut -d/ -f2-)"
fi
}

# Grabs variables set by `parse_url` and adds them to the userlist if not already set in there.
function generate_userlist_if_needed() {
if [ -n "${DB_USER}" -a -n "${DB_PASSWORD}" -a -e "${_AUTH_FILE}" ] && ! grep -q "^\"${DB_USER}\"" "${_AUTH_FILE}"; then
if [ "${AUTH_TYPE}" == "plain" ] || [ "${AUTH_TYPE}" == "scram-sha-256" ]; then
pass="${DB_PASSWORD}"
else
pass="md5$(echo -n "${DB_PASSWORD}${DB_USER}" | md5sum | cut -f 1 -d ' ')"
fi
echo "\"${DB_USER}\" \"${pass}\"" >> "${_AUTH_FILE}"
echo "Wrote authentication credentials for '${DB_USER}' to ${_AUTH_FILE}"
fi
}

# Grabs variables set by `parse_url` and adds them to the PG config file as a database entry.
function generate_config_db_entry() {
printf "\
${DB_NAME:-*} = host=${DB_HOST:?"Setup pgbouncer config error! You must set DB_HOST env"} \
port=${DB_PORT:-5432} auth_user=${DB_USER:-postgres}
${CLIENT_ENCODING:+client_encoding = ${CLIENT_ENCODING}\n}\
" >> "${PG_CONFIG_FILE}"
}

# Write the password with MD5 encryption, to avoid printing it during startup.
# Notice that `docker inspect` will show unencrypted env variables.
_AUTH_FILE="${AUTH_FILE:-$PG_CONFIG_DIR/userlist.txt}"

# Workaround userlist.txt missing issue
# https://github.com/edoburu/docker-pgbouncer/issues/33
if [ ! -e "${_AUTH_FILE}" ]; then
touch "${_AUTH_FILE}"
fi

if [ -n "$DB_USER" -a -n "$DB_PASSWORD" -a -e "${_AUTH_FILE}" ] && ! grep -q "^\"$DB_USER\"" "${_AUTH_FILE}"; then
if [ "$AUTH_TYPE" == "plain" ] || [ "$AUTH_TYPE" == "scram-sha-256" ]; then
pass="$DB_PASSWORD"
else
pass="md5$(echo -n "$DB_PASSWORD$DB_USER" | md5sum | cut -f 1 -d ' ')"
if [ -n "${DATABASE_URLS}" ]; then
echo "${DATABASE_URLS}" | tr , '\n' | while read url; do
parse_url "$url"
generate_userlist_if_needed
done
else
if [ -n "${DATABASE_URL}" ]; then
parse_url "${DATABASE_URL}"
fi
echo "\"$DB_USER\" \"$pass\"" >> ${PG_CONFIG_DIR}/userlist.txt
echo "Wrote authentication credentials to ${PG_CONFIG_DIR}/userlist.txt"
generate_userlist_if_needed
fi

if [ ! -f ${PG_CONFIG_DIR}/pgbouncer.ini ]; then
if [ ! -f "${PG_CONFIG_FILE}" ]; then
echo "Creating pgbouncer config in ${PG_CONFIG_DIR}"

# Config file is in "ini" format. Section names are between "[" and "]".
# Lines starting with ";" or "#" are taken as comments and ignored.
# The characters ";" and "#" are not recognized when they appear later in the line.
# Config file is in "ini" format. Section names are between "[" and "]".
# Lines starting with ";" or "#" are taken as comments and ignored.
# The characters ";" and "#" are not recognized when they appear later in the line.
printf "\
################## Auto generated ##################
[databases]
${DB_NAME:-*} = host=${DB_HOST:?"Setup pgbouncer config error! You must set DB_HOST env"} \
port=${DB_PORT:-5432} auth_user=${DB_USER:-postgres}
${CLIENT_ENCODING:+client_encoding = ${CLIENT_ENCODING}\n}\
" > "${PG_CONFIG_FILE}"

if [ -n "$DATABASE_URLS" ]; then
echo "$DATABASE_URLS" | tr , '\n' | while read url; do
parse_url "$url"
generate_config_db_entry
done
else
if [ -n "$DATABASE_URL" ]; then
parse_url "$DATABASE_URL"
fi
generate_config_db_entry
fi

printf "\
[pgbouncer]
listen_addr = ${LISTEN_ADDR:-0.0.0.0}
listen_port = ${LISTEN_PORT:-5432}
unix_socket_dir = ${UNIX_SOCKET_DIR}
user = postgres
auth_file = ${AUTH_FILE:-$PG_CONFIG_DIR/userlist.txt}
auth_file = ${_AUTH_FILE}
${AUTH_HBA_FILE:+auth_hba_file = ${AUTH_HBA_FILE}\n}\
auth_type = ${AUTH_TYPE:-md5}
${AUTH_USER:+auth_user = ${AUTH_USER}\n}\
Expand Down Expand Up @@ -154,9 +194,9 @@ ${TCP_KEEPIDLE:+tcp_keepidle = ${TCP_KEEPIDLE}\n}\
${TCP_KEEPINTVL:+tcp_keepintvl = ${TCP_KEEPINTVL}\n}\
${TCP_USER_TIMEOUT:+tcp_user_timeout = ${TCP_USER_TIMEOUT}\n}\
################## end file ##################
" > ${PG_CONFIG_DIR}/pgbouncer.ini
cat ${PG_CONFIG_DIR}/pgbouncer.ini
echo "Starting $*..."
" >> "${PG_CONFIG_FILE}"
cat "${PG_CONFIG_FILE}"
fi

echo "Starting $*..."
exec "$@"

0 comments on commit cea2cf8

Please sign in to comment.