More work on postgres

This commit is contained in:
Daniel Berteaud 2023-09-25 15:56:00 +02:00
parent e6f8411b2f
commit 6921bde885
20 changed files with 189 additions and 178 deletions

2
deps/common vendored

@ -1 +1 @@
Subproject commit ebefa3b9c441b0772591ce3fa667305701aa08b3
Subproject commit 292bf04d1b4007f7851b92fc42a00b9296d35453

View File

@ -15,7 +15,9 @@ RUN set -eux &&\
FROM [[ .docker.repo ]][[ .docker.base_images.alpine.image ]]
MAINTAINER [[ .docker.maintainer ]]
ENV PGHOST=localhost \
ENV LANG=[[ .locale.lang ]] \
TZ=[[ .locale.tz ]] \
PGHOST=localhost \
PGPORT=5432 \
PGUSER=postgres \
LDAP2PG_MODE=dry \

View File

@ -0,0 +1,22 @@
#!/bin/sh
set -euo pipefail
for IDX in $(printenv | grep -E '^PG_DB_([0-9]+)=' | sed -E 's/^PG_DB_([0-9]+)=.*/\1/'); do
DB_NAME=$(printenv PG_DB_${IDX})
echo "Found DB ${DB_NAME} to create"
DB_OWNER=$(printenv PG_DB_${IDX}_OWNER || echo "${DB_NAME}")
DB_ENCODING=$(printenv PG_DB_${IDX}_ENCODING || echo "UTF8")
DB_LOCALE=$(printenv PG_DB_${IDX}_LOCALE || echo "${LANG}")
echo "Create postgres role ${DB_OWNER} if needed"
psql <<_EOSQL
SELECT 'CREATE ROLE ${DB_OWNER}'
WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '${DB_OWNER}')\gexec
_EOSQL
echo "Create postgres database ${DB_NAME} (OWNER \"${DB_OWNER}\" ENCODING \"${DB_ENCODING}\" LOCALE \"${DB_LOCALE}\") if needed"
psql <<_EOSQL
SELECT 'CREATE DATABASE ${DB_NAME} OWNER "${DB_OWNER}" ENCODING "${DB_ENCODING}" LOCALE "${DB_LOCALE}"'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${DB_NAME}')\gexec
_EOSQL
done

View File

@ -1,36 +1,15 @@
#!/bin/sh
# Create DB
for IDX in $(printenv | grep -E '^PG_DB_([0-9]+)=' | sed -E 's/^PG_DB_([0-9]+)=.*/\1/'); do
DB_NAME=$(printenv PG_DB_${IDX})
echo "Found DB ${DB_NAME} to create"
DB_OWNER=$(printenv PG_DB_${IDX}_OWNER || echo "")
DB_ENCODING=$(printenv PG_DB_${IDX}_ENCODING || echo "")
DB_LOCALE=$(printenv PG_DB_${IDX}_LOCALE || echo "")
CMD="createdb"
if [ -n "${DB_OWNER}" ]; then
CMD="${CMD} --owner=${DB_OWNER}"
fi
if [ -n "${DB_ENCODING}" ]; then
CMD="${CMD} --encoding=${DB_ENCODING}"
fi
if [ -n "${DB_LOCALE}" ]; then
CMD="${CMD} --locale=${DB_LOCALE}"
fi
CMD="${CMD} ${DB_NAME}"
echo "Creating DB ${DB_NAME}"
${CMD} || echo $?
done
# Set permissions
if [ -e "${LDAP2PG_CONFIG}" ]; then
if [ "${LDAP2PG_MODE}" = "dry" ]; then
echo "Running ldap2pg in dry mode"
ldap2pg --config ${LDAP2PG_CONFIG}
elif [ "${LDAP2PG_MODE}" = "real" ]; then
if [ "${LDAP2PG_MODE}" = "real" ]; then
echo "Applying privileges with ldap2pg"
ldap2pg --real --config ${LDAP2PG_CONFIG}
else
echo "Running ldap2pg in dry mode"
ldap2pg --config ${LDAP2PG_CONFIG}
fi
# Run cron if needed
if [ -n "${LDAP2PG_CRON}" ]; then
echo "Running ldap2pg as a cron job (${LDAP2PG_CRON})"
echo "${LDAP2PG_CRON} ldap2pg --real --config ${LDAP2PG_CONFIG}" > /dev/shm/cron

View File

@ -0,0 +1,8 @@
#!/bin/sh
set -eu
# Cleanup env from the parent postgres image, as the PG_CONF_xxx are not used with patroni
for VAR in $(printenv | grep -E '^PG_CONF_' | sed -E 's/^(PG_CONF_[^=]+)=.*/\1/'); do
unset ${VAR}
done

View File

@ -0,0 +1,10 @@
#!/bin/sh
set -euo pipefail
ROLE=$(curl -k https://localhost:8080/health | jq -r .role)
if [ "${ROLE}" = "master" ]; then
exec "$@"
else
echo "Not running $@ as our instance is ${ROLE}"
fi

View File

@ -0,0 +1,27 @@
FROM [[ .docker.repo ]][[ .docker.base_images.alpine.image ]]
MAINTAINER [[ .docker.maintainer ]]
ARG PG_FROM= \
PG_TO=
ENV LANG=[[ .locale.lang ]] \
TZ=[[ .locale.tz ]]
COPY --from=walg /usr/local/bin/wal-g /usr/local/bin/wal-g
RUN set -eux &&\
apk --no-cache upgrade &&\
for VER in 12 13 14 15; do \
apk --no-cache add postgresql${PG_VERSION} \
postgresql${PG_VERSION}-client \
postgresql${PG_VERSION}-contrib \
done
apk --no-cache add icu-data-full \
tzdata &&\
mkdir -p /run/postgresql &&\
chown -R postgres:postgres /run/postgresql
COPY root/ /
USER postgres
CMD ["pg-major-upgrade"]

View File

@ -0,0 +1,56 @@
#!/bin/sh
set -euo pipefail
if [ -z "${PG_FROM}" ]; then
echo "You must set PG_FROM env var to the source version"
exit 1
elif [ -z "${PG_TO}" ]; then
echo "You must set PG_TO env var to the destination version"
exit 1
elif [ ! -d "${PG_BASE_DATA}/${PG_FROM}" ]; then
echo "Source data dir ${PG_BASE_DATA}/${PG_FROM} must already exist"
exit 1
fi
if [ -z "${DO_PG_UPGRADE}" -o "${DO_PG_UPGRADE}" != "1" ]; then
echo "Not running the upgrade. Please set DO_PG_UPGRADE=1"
fi
cd ${PG_BASE_DATA}
echo "Creating new data dir for version ${PG_TO}"
mkdir -p ${PG_BASE_DATA}/${PG_TO}
chmod 700 ${PG_BASE_DATA}/${PG_TO}
echo "Commenting SSL directives (SSL cert not available, nor needed in the upgrade context)"
cp ${PG_BASE_DATA}/${PG_FROM}/postgresql.conf ${PG_BASE_DATA}/${PG_FROM}/postgresql.conf.old
sed -i -r 's/^(ssl.*)/#\1/g' ${PG_BASE_DATA}/${PG_FROM}/postgresql.conf
echo "Replacing pg_hba with a custom one"
cp ${PG_BASE_DATA}/${PG_FROM}/pg_hba.conf ${PG_BASE_DATA}/${PG_FROM}/pg_hba.conf.old
cat <<_EOF > ${PG_BASE_DATA}/${PG_FROM}/pg_hba.conf
local all postgres peer
_EOF
echo "Initializing new PG cluster"
/usr/libexec/postgresql${PG_TO}/bin/initdb --pgdata=${PG_BASE_DATA}/${PG_TO} --auth-host=scram-sha-256 --auth-local=peer --icu-locale=${LANG} --data-checksums --encoding=UTF8 --locale-provider=icu
echo "Upgrading PG data from ${PG_BASE_DATA}/${PG_FROM} to ${PG_BASE_DATA}/${PG_TO}"
/usr/libexec/postgresql${PG_TO}/bin/pg_upgrade \
--clone \
--old-datadir ${PG_BASE_DATA}/${PG_FROM} \
--new-datadir ${PG_BASE_DATA}/${PG_TO} \
--old-bindir /usr/libexec/postgresql${PG_FROM}/bin \
--new-bindir /usr/libexec/postgresql${PG_TO}/bin
echo "Keep old patroni.dynamic.json config"
if [ -e "${PG_BASE_DATA}/${PG_FROM}/patroni.dynamic.json" ]; then
cp ${PG_BASE_DATA}/${PG_FROM}/patroni.dynamic.json ${PG_BASE_DATA}/${PG_TO}/
fi
echo "Restoring configuration"
cp -f ${PG_BASE_DATA}/${PG_FROM}/pg_hba.conf.old ${PG_BASE_DATA}/${PG_FROM}/pg_hba.conf
cp -f ${PG_BASE_DATA}/${PG_FROM}/postgresql.conf.old ${PG_BASE_DATA}/${PG_FROM}/postgresql.conf
cp -f ${PG_BASE_DATA}/${PG_FROM}/pg_hba.conf ${PG_BASE_DATA}/${PG_TO}/pg_hba.conf
cp -f ${PG_BASE_DATA}/${PG_FROM}/postgresql.conf ${PG_BASE_DATA}/${PG_TO}/postgresql.conf

View File

@ -1,50 +0,0 @@
FROM [[ .docker.repo ]][[ .docker.base_images.alpine.image ]]
MAINTAINER [[ .docker.maintainer ]]
ARG PGADMIN_VERSION=7.6
ENV GUNICORN_LIMIT_REQUEST_LINE=8190 \
GUNICORN_THREADS=25 \
PGADMIN_LISTEN_ADDRESS=0.0.0.0 \
PGADMIN_LISTEN_PORT=5050 \
PGADMIN_DATA_DIR=/data \
PYTHONPATH="/data/config:/opt/pgadmin4/venv/lib/python3.11/site-packages/pgadmin4"
#COPY --from=builder /opt/pgadmin4/venv /opt/pgadmin4/venv
RUN set -eux &&\
apk --no-cache upgrade &&\
apk --no-cache add postgresql12-client \
postgresql13-client \
postgresql14-client \
postgresql15-client \
ca-certificates \
python3 \
py3-setuptools \
&&\
apk --no-cache add --virtual build py3-pip \
gcc \
musl-dev \
python3-dev \
linux-headers \
rdfind \
&&\
python3 -m venv --system-site-packages --without-pip /opt/pgadmin4/venv &&\
/opt/pgadmin4/venv/bin/python3 -m pip install --no-cache-dir \
pgadmin4==${PGADMIN_VERSION} \
gunicorn \
&&\
find /opt/pgadmin4 -type d -name '__pycache__' -exec rm -rf {} + &&\
rm /opt/pgadmin4/venv/lib/python3.11/site-packages/pgadmin4/config_distro.py &&\
rdfind /opt/pgadmin4/venv &&\
apk del build &&\
addgroup -g 5050 pgadmin4 &&\
adduser --system --ingroup pgadmin4 --disabled-password --uid 5050 --home /opt/pgadmin4 --shell /sbin/nologin pgadmin4 &&\
mkdir /data &&\
chown pgadmin4:pgadmin4 /data &&\
chmod 750 /data
COPY root/ /
EXPOSE ${PGADMIN_LISTEN_PORT}
USER pgadmin4
CMD ["pgadmin4"]

View File

@ -1,50 +0,0 @@
#!/bin/sh
set -e
mkdir -p ${PGADMIN_DATA_DIR}/config
# Populate config_local.py from env vars
cat <<_EOF > ${PGADMIN_DATA_DIR}/config/config_distro.py
CA_FILE = '/etc/ssl/certs/ca-certificates.crt'
DEFAULT_BINARY_PATHS = {
'pg': '/usr/libexec/postgresql15',
'pg-15': '/usr/libexec/postgresql15',
'pg-14': '/usr/libexec/postgresql14',
'pg-13': '/usr/libexec/postgresql13',
'pg-12': '/usr/libexec/postgresql12',
}
DATA_DIR = '${PGADMIN_DATA_DIR}'
LOG_FILE = '/dev/stdout'
_EOF
chmod 640 ${PGADMIN_DATA_DIR}/config/config_distro.py
for VAR in $(printenv | grep -E "^PGADMIN_CONFIG_" | sed -E 's/^PGADMIN_CONFIG_([^=]+)=.*/\1/'); do
echo "Adding ${VAR} = $(printenv PGADMIN_CONFIG_${VAR}) in config_distro.py"
echo "${VAR} = $(printenv PGADMIN_CONFIG_${VAR})" >> ${PGADMIN_DATA_DIR}/config/config_distro.py
done
if [ \! -f ${PGADMIN_DATA_DIR}/pgadmin4.db ]; 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
if ! echo "${PGADMIN_DEFAULT_EMAIL}" | grep -q -E "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$" > /dev/null; 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."
exit 1
fi
# Read secret contents
if [ -n "${PGADMIN_DEFAULT_PASSWORD_FILE}" ]; then
PGADMIN_DEFAULT_PASSWORD=$(cat "${PGADMIN_DEFAULT_PASSWORD_FILE}")
export PGADMIN_DEFAULT_PASSWORD
fi
# 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}"
fi

View File

@ -1,12 +0,0 @@
#!/bin/sh
set -e
exec /opt/pgadmin4/venv/bin/gunicorn \
--limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE}" \
--bind "${PGADMIN_LISTEN_ADDRESS}:${PGADMIN_LISTEN_PORT}" \
--workers 1 \
--threads "${GUNICORN_THREADS}" \
--access-logfile '-' \
--error-logfile '-' \
pgAdmin4:app

View File

@ -1,6 +1,6 @@
#!/bin/sh
for USER in pg monitor replicator rewind api; do
for USER in pg monitor replicator rewind api vault_initial; do
vault kv get -field ${USER}_pwd [[ .vault.prefix ]]kv/service/[[ .pg.job_name ]] > /dev/null 2>&1
RES=$?
if [ "${RES}" = "1" ]; then

View File

@ -1,7 +1,7 @@
#!/bin/sh
[[ template "common/mkpki.sh.tpl" dict
[[ template "common/vault.mkpki.sh.tpl" dict
"ctx" .
"pki" (dict
"name" .pg.job_name

18
init/vault-database Executable file
View File

@ -0,0 +1,18 @@
#!/bin/sh
if [ "$(vault secrets list -format json | jq -r '.["[[ .vault.prefix ]]database/"].type')" != "database" ]; then
INITIAL_SETUP=true
fi
if [ "${INITIAL_SETUP}" = "true" ]; then
vault secrets enable -path [[ .vault.prefix ]]database database
vault write [[ .vault.prefix ]]database/config/[[ .pg.job_name ]] \
plugin_name="postgresql-database-plugin" \
connection_url="postgresql://{{username}}:{{password}}@[[ (urlParse .pg.server.traefik.public_address).Host ]]/postgres" \
allowed_roles="*" \
username=vault \
password="$(vault kv get -field vault_initial_pwd [[ .vault.prefix ]]kv/service/[[ .pg.job_name ]])" \
password_authentication=scram-sha-256 \
disable_escaping=true
vault write -force [[ .vault.prefix ]]database/rotate-root/[[ .pg.job_name ]]
fi

View File

@ -9,6 +9,12 @@ job [[ .pg.job_name | toJSON ]] {
size = 101
}
# Force different instances to run on distinct nodes
constraint {
operator = "distinct_hosts"
value = "true"
}
count = [[ .pg.server.recovery | ternary 1 .pg.server.count ]]
network {
mode = "bridge"
@ -88,6 +94,7 @@ job [[ .pg.job_name | toJSON ]] {
enable_tag_override = true
}
[[- if .prometheus.enabled ]]
# This service is just used to expose patroni metrics
service {
name = "[[ .pg.job_name ]]-patroni[[ .env.suffix ]]"
@ -98,6 +105,7 @@ job [[ .pg.job_name | toJSON ]] {
alloc = "${NOMAD_ALLOC_INDEX}"
}
}
[[- end ]]
[[- end ]]
volume "postgres" {
@ -135,11 +143,10 @@ job [[ .pg.job_name | toJSON ]] {
template {
data = <<_EOT
PG_PASSWORD={{ with secret "[[ .vault.prefix ]]kv/service/[[ .pg.job_name ]]" }}{{ .Data.data.pg_pwd }}{{ end }}
# Get a Consul token from vault, so we're able to update the tags in Consul from the containers
CONSUL_HTTP_TOKEN={{ with secret "[[ .vault.prefix ]]consul/creds/[[ .pg.job_name ]]" }}{{ .Data.token }}{{ end }}
PATRONICTL_CONFIG_FILE = "/secrets/patroni.yml"
PGBACKREST_STANZA = [[ .pg.job_name | toJSON ]]
PATRONICTL_CONFIG_FILE=/secrets/patroni.yml
PGBACKREST_STANZA=[[ .pg.job_name ]]
_EOT
destination = "secrets/pg.env"
uid = 100000
@ -155,6 +162,7 @@ _EOT
[[ template "postgres/serviceformat.jq.tpl" . ]]
_EOT
destination = "local/serviceformat.jq"
change_mode = "noop"
}
template {
@ -162,7 +170,8 @@ _EOT
[[ template "postgres/update_tags.sh.tpl" . ]]
_EOT
destination = "local/update_tags.sh"
perms = 755
perms = 755
change_mode = "noop"
}
# Patroni main configuration file
@ -224,7 +233,7 @@ _EOT
image = [[ .pg.server.image | toJSON ]]
command = "supercronic"
args = [
"secrets/backup.cron"
"/secrets/backup.cron"
]
readonly_rootfs = true
pids_limit = 100
@ -253,7 +262,6 @@ _EOT
volume_mount {
volume = "postgres"
destination = "/data"
read_only = true
}
[[ template "common/resources.tpl" .pg.backup.resources ]]
@ -314,5 +322,6 @@ _EOT
[[- if and (not .pg.server.recovery) (has .pg.ldap2pg "cron") (ne .pg.ldap2pg.cron "") ]]
[[ template "postgres/group.ldap2pg.tpl" . ]]
[[- end ]]
}

View File

@ -0,0 +1,9 @@
[[- if ne .pg.backup.cron.full "" ]]
[[ .pg.backup.cron.full ]] run-if-master.sh pgbackrest backup --delta --type=full
[[- end ]]
[[- if ne .pg.backup.cron.incr "" ]]
[[ .pg.backup.cron.incr ]] run-if-master.sh pgbackrest backup --delta --type=incr
[[- end ]]
[[- if ne .pg.backup.cron.diff "" ]]
[[ .pg.backup.cron.diff ]] run-if-master.sh pgbackrest backup --delta --type=diff
[[- end ]]

View File

@ -42,6 +42,3 @@ privileges:
- __create_on_schemas__
- __truncate_on_tables__
rewinder:
- __connect__
- __execute_on_functions__

View File

@ -25,18 +25,6 @@ rules:
- pg_monitor
- managed_roles
- name: replicator
comment: Postgres replication account
options: LOGIN REPLICATION
parents:
- managed_roles
- name: rewind
comment: "Replication rewinder"
parents:
- managed_roles
options: LOGIN
- name: dba
comment: "Databases admins"
options: SUPERUSER NOLOGIN
@ -48,7 +36,7 @@ rules:
parents: managed_roles
- grant:
role: rewind
privileges:
- rewinder
role: vault
privileges: reader
databases: postgres

View File

@ -34,6 +34,10 @@ bootstrap:
- replication
rewind:
password: '{{ .Data.data.rewind_pwd }}'
vault:
password: '{{ .Data.data.vault_initial_pwd }}'
options:
- createrole
{{ end }}
postgresql:

View File

@ -30,6 +30,7 @@ pg:
maintenance_work_mem: 5%
work_mem: 1%
archive_timeout: 900
wal_keep_size: 512
driver: docker
resources:
cpu: 500
@ -40,6 +41,7 @@ pg:
source: postgres
traefik:
enabled: True
public_address: postgres://postgres.example.org:5432
entrypoints:
- postgres
# host: postgres.lapiole.org
@ -50,9 +52,9 @@ pg:
enabled: False
env: {}
cron:
full: 15 02 * * 0
incr: 15 02 * * 1-6
diff: ""
full: 15 02 1 * *
diff: 15 02 2-31 * 0
incr: 15 02 2-31 * 1-6
resources:
cpu: 100
memory: 50
@ -66,20 +68,12 @@ pg:
cpu: 20
memory: 32
pgadmin:
image: danielberteaud/pgadmin4:latest
driver: docker
resources:
cpu: 100
memory: 256
env: {}
ldap2pg:
image: danielberteaud/ldap2pg:latest
driver: docker
resources:
cpu: 20
memory: 20
memory: 32
env: {}
connect:
upstreams: