diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 76fa9ce..3f7c03a 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -12,7 +12,7 @@ concurrency: permissions: read-all jobs: - docker: + docker-build: runs-on: ubuntu-26.04 strategy: matrix: @@ -34,7 +34,7 @@ jobs: docker images - test: + end-to-end: runs-on: ubuntu-26.04 steps: - uses: actions/checkout@v7 diff --git a/17/Dockerfile b/17/Dockerfile index 71ac39c..b45f15a 100644 --- a/17/Dockerfile +++ b/17/Dockerfile @@ -1,3 +1,12 @@ +# Copyright (c) 2026 Alexander Todorov +# +# Licensed under GNU Affero General Public License v3 or later (AGPLv3+) +# https://www.gnu.org/licenses/agpl-3.0.html + FROM postgres:17@sha256:e38411452a464af89e5adadb8d223bf53b898d47d6ef918b2d58c08707350449 COPY ./docker-entrypoint-initdb.d/00_enable_ssl.sh /docker-entrypoint-initdb.d/ + +COPY entrypoint.sh /usr/local/bin/ +ENTRYPOINT ["entrypoint.sh"] +CMD ["postgres"] diff --git a/17/entrypoint.sh b/17/entrypoint.sh new file mode 100755 index 0000000..0479f07 --- /dev/null +++ b/17/entrypoint.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +# Copyright (c) 2026 Alexander Todorov +# +# Licensed under GNU Affero General Public License v3 or later (AGPLv3+) +# https://www.gnu.org/licenses/agpl-3.0.html + +echo "INFO: check if replication is configured" +if [ ! -s "$PGDATA/PG_VERSION" ] && [ -n "$POSTGRES_REPLICATION_USER" ]; then + mkdir -p "$PGDATA" + chmod 00700 "$PGDATA" || : + + if [ -z "$POSTGRES_REPLICATION_PASSWORD" ]; then + echo "ERROR: POSTGRES_REPLICATION_PASSWORD is undefined" + exit 1 + fi + + if [ -z "$POSTGRES_PRIMARY_HOST" ]; then + echo "ERROR: POSTGRES_PRIMARY_HOST is undefined" + exit 2 + fi + + echo "INFO: starting initial wal sync" + pg_basebackup \ + --dbname "postgres://$POSTGRES_REPLICATION_USER:$POSTGRES_REPLICATION_PASSWORD@$POSTGRES_PRIMARY_HOST/postgres?sslmode=require" \ + --pgdata "$PGDATA" \ + --progress --verbose --write-recovery-conf --wal-method stream \ + --create-slot --slot "$POSTGRES_REPLICATION_USER" + + echo "INFO: completed initial wal sync" +fi + +echo "INFO: starting postgres" +exec docker-entrypoint.sh "$@" diff --git a/18/Dockerfile b/18/Dockerfile index b50907f..f5bdee8 100644 --- a/18/Dockerfile +++ b/18/Dockerfile @@ -1,3 +1,7 @@ FROM postgres:18@sha256:06cad38a5d9f5d24b4d83d86def30795d5e4b757fedbf5281172b576dedcd941 COPY ./docker-entrypoint-initdb.d/00_enable_ssl.sh /docker-entrypoint-initdb.d/ + +COPY entrypoint.sh /usr/local/bin/ +ENTRYPOINT ["entrypoint.sh"] +CMD ["postgres"] diff --git a/18/entrypoint.sh b/18/entrypoint.sh new file mode 100755 index 0000000..0479f07 --- /dev/null +++ b/18/entrypoint.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +# Copyright (c) 2026 Alexander Todorov +# +# Licensed under GNU Affero General Public License v3 or later (AGPLv3+) +# https://www.gnu.org/licenses/agpl-3.0.html + +echo "INFO: check if replication is configured" +if [ ! -s "$PGDATA/PG_VERSION" ] && [ -n "$POSTGRES_REPLICATION_USER" ]; then + mkdir -p "$PGDATA" + chmod 00700 "$PGDATA" || : + + if [ -z "$POSTGRES_REPLICATION_PASSWORD" ]; then + echo "ERROR: POSTGRES_REPLICATION_PASSWORD is undefined" + exit 1 + fi + + if [ -z "$POSTGRES_PRIMARY_HOST" ]; then + echo "ERROR: POSTGRES_PRIMARY_HOST is undefined" + exit 2 + fi + + echo "INFO: starting initial wal sync" + pg_basebackup \ + --dbname "postgres://$POSTGRES_REPLICATION_USER:$POSTGRES_REPLICATION_PASSWORD@$POSTGRES_PRIMARY_HOST/postgres?sslmode=require" \ + --pgdata "$PGDATA" \ + --progress --verbose --write-recovery-conf --wal-method stream \ + --create-slot --slot "$POSTGRES_REPLICATION_USER" + + echo "INFO: completed initial wal sync" +fi + +echo "INFO: starting postgres" +exec docker-entrypoint.sh "$@" diff --git a/docker-compose.yml b/docker-compose.yml index 0ecf5e9..b927bf4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,7 @@ services: restart: always volumes: - db17_data:/var/lib/postgresql + - ./initdb.d/01_replication_users.sh:/docker-entrypoint-initdb.d/01_replication_users.sh:Z environment: POSTGRES_DB: kiwitcms POSTGRES_USER: kiwitcms @@ -73,6 +74,7 @@ services: dockerfile: Dockerfile volumes: - db18_data:/var/lib/postgresql + - ./initdb.d/01_replication_users.sh:/docker-entrypoint-initdb.d/01_replication_users.sh:Z web: container_name: web @@ -99,3 +101,8 @@ services: volumes: db17_data: db18_data: + + +networks: + default: + name: postgres_default diff --git a/initdb.d/01_replication_users.sh b/initdb.d/01_replication_users.sh new file mode 100755 index 0000000..bafd14d --- /dev/null +++ b/initdb.d/01_replication_users.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Copyright (c) 2026 Alexander Todorov +# +# Licensed under GNU Affero General Public License v3 or later (AGPLv3+) +# https://www.gnu.org/licenses/agpl-3.0.html + +set -eu + +replication_user() { + local username=$1 + local password=$2 + + psql --set ON_ERROR_STOP=1 \ + --set=username="$username" \ + --set=password="$password" \ + --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + CREATE USER :"username" WITH ENCRYPTED PASSWORD :'password' REPLICATION; +EOSQL +} + +replication_user "rpl_usr_17" "replicate-me" +replication_user "rpl_usr_18" "replicate-me" diff --git a/testing/test_docker.sh b/testing/test_docker.sh index d4afb61..6db884e 100755 --- a/testing/test_docker.sh +++ b/testing/test_docker.sh @@ -37,6 +37,16 @@ rlJournalStart rlAssertGrep "pg_hba.conf rejects connection .* no encryption" "$rlRun_LOG" rlPhaseEnd + rlPhaseStartTest "Start replication containers" + rlRun -t -c "docker run -d --name=replica_17 --network=postgres_default -e POSTGRES_REPLICATION_USER=rpl_usr_17 -e POSTGRES_REPLICATION_PASSWORD=replicate-me -e POSTGRES_PRIMARY_HOST=postgres_17 postgres-postgres_17:latest" + sleep 120 + rlRun -t -c "docker logs replica_17" + + rlRun -t -c "docker run -d --name=replica_18 --network=postgres_default -e POSTGRES_REPLICATION_USER=rpl_usr_18 -e POSTGRES_REPLICATION_PASSWORD=replicate-me -e POSTGRES_PRIMARY_HOST=postgres_18 postgres-postgres_18:latest" + sleep 120 + rlRun -t -c "docker logs replica_18" + rlPhaseEnd + rlPhaseStartTest "Container restart" rlRun -t -c "docker compose restart" assert_up_and_running @@ -56,7 +66,21 @@ rlJournalStart assert_up_and_running rlPhaseEnd + rlPhaseStartTest "Check content in replicated databases" + REPLICA_17_ADDRESS=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' replica_17) + rlRun -t -c "psql --dbname 'postgres://kiwitcms:kiwitcms@$REPLICA_17_ADDRESS/kiwitcms?sslmode=require' -c 'SELECT * FROM management_priority;' | grep '5 rows'" + + REPLICA_18_ADDRESS=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' replica_18) + rlRun -t -c "psql --dbname 'postgres://kiwitcms:kiwitcms@$REPLICA_18_ADDRESS/kiwitcms?sslmode=require' -c 'SELECT * FROM management_priority;' | grep '5 rows'" + rlPhaseEnd + rlPhaseStartCleanup + rlRun -t -c "docker kill replica_17" + rlRun -t -c "docker rm replica_17" + + rlRun -t -c "docker kill replica_18" + rlRun -t -c "docker rm replica_18" + rlRun -t -c "docker compose down" if [ -n "$ImageOS" ]; then rlRun -t -c "docker volume rm postgres_db17_data"