From a71779fce7b518956919fbe1925b69894b84cd0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sat, 13 Jun 2026 17:59:54 +0200 Subject: [PATCH 01/15] feat(dbserver): migrate to stock postgres image + bind-mounted autotune The custom iplweb/bpp_dbserver image is discontinued (its only delta over stock postgres was the autotune step). Switch dbserver to the official Debian-based postgres: image with the autotune scripts bind-mounted from dbserver/ (copied verbatim from iplweb/bpp-dbserver). docker-compose.database.yml: - image -> postgres:${DJANGO_BPP_POSTGRESQL_VERSION...} (two-tier fallback kept) - autotune entrypoint + command: postgres; scripts bind-mounted read-only - pin PGDATA=/var/lib/postgresql/data (stock postgres:18+ defaults elsewhere -> would ignore the existing volume and re-init blank) - POSTGRES_INITDB_ARGS ICU pl-PL for fresh installs (never re-collates existing) - pg_isready healthcheck (stock postgres has none; appserver/authserver depend on service_healthy) - service-level env_file (the include-level env_file is interpolation-only and is not injected into the container) so autotune POSTGRESQL_* knobs are honoured upgrade-postgres.sh / test-upgrade-postgres.sh: - NEW_DBSERVER_IMAGE -> postgres:; drop the "wait for image publish" step - test harness uses the stock multi-arch image (drop platform: + DOCKER_DEFAULT_PLATFORM -> runs native on Apple Silicon) and mirrors prod (autotune mounts, PGDATA pin, healthcheck) init-configs.sh / check-image-versions.sh: message/comment updates only (version-var logic and old-name migrations unchanged). Docs: postgresql.md, limity-zasobow.md (autotune reads the cgroup limit), CLAUDE.md, plus the design spec under docs/superpowers/specs/. Verified: compose config, autotune --test, shellcheck, mkdocs --strict, and a live boot of postgres:16.13 (healthy; shared_buffers sized to the cgroup limit; ICU pl-PL collation; PGDATA pinned). Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 2 +- dbserver/autotune.sh | 272 ++++++++++++++++++ dbserver/docker-entrypoint-autotune.sh | 34 +++ docker-compose.database.yml | 58 +++- docs/konfiguracja/limity-zasobow.md | 11 +- docs/konfiguracja/postgresql.md | 29 +- ...dbserver-stock-postgres-autotune-design.md | 256 +++++++++++++++++ scripts/check-image-versions.sh | 5 +- scripts/init-configs.sh | 16 +- scripts/test-upgrade-postgres.sh | 42 ++- scripts/upgrade-postgres.sh | 18 +- 11 files changed, 695 insertions(+), 48 deletions(-) create mode 100755 dbserver/autotune.sh create mode 100755 dbserver/docker-entrypoint-autotune.sh create mode 100644 docs/superpowers/specs/2026-06-13-dbserver-stock-postgres-autotune-design.md diff --git a/CLAUDE.md b/CLAUDE.md index 0d972b4..2d58995 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,7 +83,7 @@ User uploads land in the `media` volume mounted at `/mediaroot` in every Django ### PostgreSQL version vars -`dbserver` uses `iplweb/bpp_dbserver:psql-${DJANGO_BPP_POSTGRESQL_VERSION}` (`MAJOR.MINOR`, default `16.13`). `DJANGO_BPP_POSTGRESQL_VERSION_MAJOR` (auto-derived) is used by `backup-runner` (`postgres:-alpine`). Major upgrades require dump/restore — use `make upgrade-postgres`, do **not** edit the var manually. Full procedure (rollback, resume): `docs/konfiguracja/postgresql.md`. +`dbserver` uses the **stock official** `postgres:${DJANGO_BPP_POSTGRESQL_VERSION}` image (Debian, **not** `-alpine` — the entrypoint needs `bash`; `MAJOR.MINOR`, default `16.13`) with the autotune scripts in **`dbserver/`** (`autotune.sh` + `docker-entrypoint-autotune.sh`, copied verbatim from `iplweb/bpp-dbserver`) **bind-mounted** read-only on top. The old `iplweb/bpp_dbserver` image is **discontinued** — autotune was its only delta over stock postgres. These scripts are versioned code delivered by `git pull` — **not** force-synced into `$BPP_CONFIGS_DIR`. CRITICAL contracts: (1) `PGDATA` is pinned to `/var/lib/postgresql/data` (stock `postgres:18+` defaults to a versioned subdir → would ignore the existing volume and re-init blank — never change the mount to `/var/lib/postgresql`); (2) fresh installs init with `POSTGRES_INITDB_ARGS=--locale-provider=icu --icu-locale=pl-PL` (Polish collation; **fresh PGDATA only**, never re-collates existing clusters); (3) stock postgres has no built-in healthcheck, so `dbserver` defines its own `pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"` (appserver/authserver `depend_on: service_healthy`); (4) `dbserver` needs a **service-level** `env_file: ${BPP_CONFIGS_DIR}/.env` — the `include`-level `env_file` is interpolation-only and is NOT injected into the container. `DJANGO_BPP_POSTGRESQL_VERSION_MAJOR` (auto-derived) is used by `backup-runner` (`postgres:-alpine`). Major upgrades require dump/restore — use `make upgrade-postgres`, do **not** edit the var manually. Full procedure (rollback, resume): `docs/konfiguracja/postgresql.md`. ## Critical Deployment Patterns diff --git a/dbserver/autotune.sh b/dbserver/autotune.sh new file mode 100755 index 0000000..88853c9 --- /dev/null +++ b/dbserver/autotune.sh @@ -0,0 +1,272 @@ +#!/bin/sh +# Shell port of autotune.py — generates a pgtune-style postgresql.conf include +# on stdout (cgroup/meminfo aware), with NO Python dependency. +# +# This lets the autotune step run on the *stock* `postgres` image via a +# bind-mounted entrypoint (no custom image / no python3 install needed). +# +# It is a faithful port of autotune.py: output is verified byte-identical +# (see `./autotune.sh --test` and the parity check in CI). Floating-point math +# is delegated to awk, which uses the same IEEE-754 doubles as Python, so +# truncation (int()) and unit normalization match exactly. +# +# Quirks deliberately preserved from autotune.py: +# * POSTGRESQL_RAM_THIS_MUCH_GB is actually treated as MB (as in the original) +# * the "tweak it orremove it" typo and POSTGRESQL_RAM_THIS_MUCH_DB typo in +# the forced-RAM comment +# * int(POSTGRESQL_RAM_PERCENT * 100) for the "%" shown in comments (0.95 -> 94) +set -eu + +ONE_GB_IN_KB=1048576 # 1024 * 1024 + +# --- environment (same names / defaults as autotune.py) ----------------------- +RAM_PERCENT="${POSTGRESQL_RAM_PERCENT:-0.95}" +FORCE_RAM="${POSTGRESQL_RAM_THIS_MUCH_GB:-}" +DEFAULT_RAM="${POSTGRESQL_DEFAULT_RAM:-4096}" +UNSAFE_RAW="${POSTGRESQL_UNSAFE_BUT_FAST:-}" +MAX_LOCKS="${POSTGRESQL_MAX_LOCKS_PER_TRANSACTION:-}" +MAX_PRED_LOCKS="${POSTGRESQL_MAX_PRED_LOCKS_PER_TRANSACTION:-}" + +# unsafe flag: POSTGRESQL_UNSAFE_BUT_FAST.lower() in (1, true, yes) +UNSAFE=0 +case "$(printf '%s' "$UNSAFE_RAW" | tr '[:upper:]' '[:lower:]')" in + 1 | true | yes) UNSAFE=1 ;; +esac + +# --- RAM / CPU detection ------------------------------------------------------ + +# /proc/meminfo always shows host RAM, not the container limit — the limit lives +# in the cgroup. Echoes the limit in kB, or nothing when there is no finite limit. +cgroup_limit_kb() { + if [ -f /sys/fs/cgroup/memory.max ]; then + cg_raw=$(tr -d '[:space:]' < /sys/fs/cgroup/memory.max 2>/dev/null || true) + if [ -n "$cg_raw" ] && [ "$cg_raw" != "max" ]; then + awk -v r="$cg_raw" 'BEGIN { printf "%d", int(r / 1024) }' + return 0 + fi + fi + if [ -f /sys/fs/cgroup/memory/memory.limit_in_bytes ]; then + cg_v=$(tr -d '[:space:]' < /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null || true) + # cgroup v1 returns a huge sentinel value when no limit is set (>= 1<<62) + if [ -n "$cg_v" ]; then + awk -v v="$cg_v" 'BEGIN { if (v + 0 < 4611686018427387904) printf "%d", int(v / 1024) }' + fi + fi +} + +host_ram_kb() { + [ -f /proc/meminfo ] || return 0 + awk '/^MemTotal:/ { print $2; exit }' /proc/meminfo +} + +detect_nproc() { + # Match Python's multiprocessing.cpu_count() == os.cpu_count() == online CPUs. + np=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo "") + case "$np" in '' | *[!0-9]*) np=$(nproc 2>/dev/null || echo "") ;; esac + case "$np" in '' | *[!0-9]*) np=1 ;; esac + printf '%s' "$np" +} + +# Sets globals RAM_KB (full-precision double as %.17g) and RAM_COMMENT. +resolve_ram_kb() { + if [ -n "$FORCE_RAM" ]; then + RAM_COMMENT="# autotune.py: RAM size for Postgres is ${FORCE_RAM} MB, because of env var POSTGRESQL_RAM_THIS_MUCH_DB setting. Change the env var to tweak it orremove it to use automatic RAM size detection." + RAM_KB=$(awk -v f="$FORCE_RAM" 'BEGIN { printf "%.17g", int(f) * 1024 }') + return 0 + fi + + host_kb=$(host_ram_kb) + cg_kb=$(cgroup_limit_kb) + pct100=$(awk -v p="$RAM_PERCENT" 'BEGIN { printf "%d", int(p * 100) }') + + if [ -n "$cg_kb" ] && { [ -z "$host_kb" ] || [ "$cg_kb" -lt "$host_kb" ]; }; then + RAM_COMMENT="# autotune.py: detected cgroup memory limit ${cg_kb} kB (host RAM: ${host_kb:-None} kB); will use ${pct100} % of the cgroup limit" + RAM_KB=$(awk -v p="$RAM_PERCENT" -v c="$cg_kb" 'BEGIN { printf "%.17g", (p * c / 1024) * 1024 }') + return 0 + fi + + if [ -n "$host_kb" ]; then + RAM_COMMENT="# autotune.py: detected ${host_kb} kB RAM; will use ${pct100} % of it" + RAM_KB=$(awk -v p="$RAM_PERCENT" -v h="$host_kb" 'BEGIN { printf "%.17g", (p * h / 1024) * 1024 }') + return 0 + fi + + RAM_COMMENT="# autotune.py: unable to detect RAM size, returning default ${DEFAULT_RAM} MB; change environment variable POSTGRESQL_DEFAULT_RAM if you need to change this" + RAM_KB=$(awk -v d="$DEFAULT_RAM" 'BEGIN { printf "%.17g", d * 1024 }') +} + +# --- config generation -------------------------------------------------------- + +# Emits unsorted "key = value" config lines for a given RAM size in kB ($1). +# Reads globals: NPROC, MAX_PARALLEL, CFG_UNSAFE, MAX_LOCKS, MAX_PRED_LOCKS. +generate_config_lines() { + awk -v ramkb="$1" -v nproc="$NPROC" -v max_parallel="$MAX_PARALLEL" \ + -v unsafe="$CFG_UNSAFE" -v maxlocks="$MAX_LOCKS" -v maxpredlocks="$MAX_PRED_LOCKS" ' + function normsize(x, iv) { + iv = int(x) + if (iv % GB == 0) return sprintf("%dGB", iv / GB) + if (iv % MB == 0) return sprintf("%dMB", iv / MB) + return sprintf("%dkB", iv) + } + BEGIN { + GB = 1048576; MB = 1024 + + # Directly from pgtune + printf "shared_buffers = %s\n", normsize(ramkb / 4) + printf "effective_cache_size = %s\n", normsize(ramkb * 3 / 4) + mwm = ramkb / 16; if (mwm > 2 * GB) mwm = 2 * GB + printf "maintenance_work_mem = %s\n", normsize(mwm) + + # 100 connections per 1 GB RAM, capped at 250 + conns = 100 * ramkb / GB; if (conns > 250) conns = 250 + printf "max_connections = %d\n", int(conns) + printf "work_mem = %s\n", normsize((ramkb * 3 / 4) / (conns * 3) / max_parallel) + + printf "min_wal_size = %s\n", normsize(GB) + printf "max_wal_size = %s\n", normsize(4 * GB) + + wb = ramkb * 3 / 4 / 100; if (wb > 16 * MB) wb = 16 * MB + printf "wal_buffers = %s\n", normsize(wb) + + print "checkpoint_completion_target = 0.7" + print "default_statistics_target = 100" + + printf "max_worker_processes = %d\n", nproc + printf "max_parallel_workers_per_gather = %d\n", max_parallel + printf "max_parallel_workers = %d\n", nproc + printf "max_parallel_maintenance_workers = %d\n", max_parallel + + if (maxlocks != "") printf "max_locks_per_transaction = %d\n", maxlocks + 0 + if (maxpredlocks != "") printf "max_pred_locks_per_transaction = %d\n", maxpredlocks + 0 + + if (unsafe) { + print "fsync = off" + print "full_page_writes = off" + print "synchronous_commit = off" + print "wal_level = minimal" + print "max_wal_senders = 0" + print "archive_mode = off" + print "wal_writer_delay = 10000ms" + print "commit_delay = 100000" + print "random_page_cost = 1.1" + print "effective_io_concurrency = 200" + } + } + ' +} + +# max_parallel mirrors autotune.py: 1 (<4 cpus), 2 (>=4), 3 (5-6), 4 (>=7) +compute_max_parallel() { + MAX_PARALLEL=1 + if [ "$NPROC" -ge 4 ]; then + MAX_PARALLEL=2 + if [ "$NPROC" -ge 5 ] && [ "$NPROC" -le 6 ]; then MAX_PARALLEL=3; fi + if [ "$NPROC" -ge 7 ]; then MAX_PARALLEL=4; fi + fi +} + +main() { + resolve_ram_kb + NPROC=$(detect_nproc) + compute_max_parallel + CFG_UNSAFE="$UNSAFE" + + printf '%s\n' "$RAM_COMMENT" + printf '# Automatically added by autotune.py\n' + if [ "$UNSAFE" -eq 1 ]; then + cat <<'EOF' +# +# *** UWAGA! TRYB POSTGRESQL_UNSAFE_BUT_FAST JEST WŁĄCZONY! *** +# *** fsync, full_page_writes, synchronous_commit WYŁĄCZONE *** +# *** wal_level=minimal, max_wal_senders=0, archive_mode=off *** +# *** DANE MOGĄ ZOSTAĆ UTRACONE! NIE UŻYWAJ W PRODUKCJI! *** +# +EOF + fi + generate_config_lines "$RAM_KB" | LC_ALL=C sort +} + +# --- self-test (mirrors autotune.py test()) ----------------------------------- + +TEST_FAIL=0 + +assert_value() { + # $1=size kB $2=key $3=expected ; reads $TEST_CFG + av_got=$(printf '%s\n' "$TEST_CFG" | sed -n "s/^$2 = //p") + if [ "$av_got" != "$3" ]; then + printf 'FAIL: Postgres at %s kB: %s differs:\n Got: %s\n Expected: %s\n' \ + "$1" "$2" "$av_got" "$3" >&2 + TEST_FAIL=1 + fi +} + +assert_present() { + # $1=size kB $2=key ; reads $TEST_CFG + if ! printf '%s\n' "$TEST_CFG" | grep -q "^$2 = "; then + printf 'FAIL: Postgres at %s kB: missing key %s\n' "$1" "$2" >&2 + TEST_FAIL=1 + fi +} + +run_tests() { + NPROC=$(detect_nproc) + compute_max_parallel + CFG_UNSAFE=0 + # locks must not leak into the deterministic cases + saved_locks="$MAX_LOCKS"; saved_pred="$MAX_PRED_LOCKS" + MAX_LOCKS=""; MAX_PRED_LOCKS="" + + # RAM-dependent (deterministic) values. work_mem and CPU-dependent keys depend + # on cpu count / max_connections, so we only check their presence (as in py). + for tc in \ + "524288|shared_buffers=128MB effective_cache_size=384MB maintenance_work_mem=32MB min_wal_size=1GB max_wal_size=4GB checkpoint_completion_target=0.7 wal_buffers=3932kB default_statistics_target=100 max_connections=50" \ + "1048576|shared_buffers=256MB effective_cache_size=768MB maintenance_work_mem=64MB min_wal_size=1GB max_wal_size=4GB checkpoint_completion_target=0.7 wal_buffers=7864kB default_statistics_target=100 max_connections=100" \ + "2097152|shared_buffers=512MB effective_cache_size=1536MB maintenance_work_mem=128MB min_wal_size=1GB max_wal_size=4GB checkpoint_completion_target=0.7 wal_buffers=15728kB default_statistics_target=100 max_connections=200" \ + "4194304|shared_buffers=1GB effective_cache_size=3GB maintenance_work_mem=256MB min_wal_size=1GB max_wal_size=4GB checkpoint_completion_target=0.7 wal_buffers=16MB default_statistics_target=100 max_connections=250"; do + size=${tc%%|*} + pairs=${tc#*|} + TEST_CFG=$(generate_config_lines "$size") + for p in $pairs; do + assert_value "$size" "${p%%=*}" "${p#*=}" + done + for k in max_worker_processes max_parallel_workers_per_gather \ + max_parallel_workers max_parallel_maintenance_workers work_mem; do + assert_present "$size" "$k" + done + done + + MAX_LOCKS="$saved_locks"; MAX_PRED_LOCKS="$saved_pred" + + # unsafe-mode test (only when the env flag is on, as in autotune.py) + if [ "$UNSAFE" -eq 1 ]; then + CFG_UNSAFE=1 + TEST_CFG=$(generate_config_lines "$ONE_GB_IN_KB") + for k in fsync full_page_writes synchronous_commit archive_mode; do + assert_value "$ONE_GB_IN_KB" "$k" "off" + done + assert_value "$ONE_GB_IN_KB" wal_level minimal + assert_value "$ONE_GB_IN_KB" max_wal_senders 0 + assert_value "$ONE_GB_IN_KB" random_page_cost 1.1 + assert_value "$ONE_GB_IN_KB" effective_io_concurrency 200 + assert_present "$ONE_GB_IN_KB" wal_writer_delay + assert_present "$ONE_GB_IN_KB" commit_delay + fi + + if [ "$TEST_FAIL" -ne 0 ]; then + exit 1 + fi + printf 'OK\n' >&2 +} + +usage() { + printf 'Usage: %s [--test]\n' "$1" >&2 +} + +# --- entrypoint --------------------------------------------------------------- +if [ "$#" -eq 0 ]; then + main +elif [ "$#" -eq 1 ] && [ "$1" = "--test" ]; then + run_tests +else + usage "$0" +fi diff --git a/dbserver/docker-entrypoint-autotune.sh b/dbserver/docker-entrypoint-autotune.sh new file mode 100755 index 0000000..16a9249 --- /dev/null +++ b/dbserver/docker-entrypoint-autotune.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Autotune entrypoint wrapper. Python-free — it shells out to autotune.sh, so it +# runs equally well INSIDE a custom image or BIND-MOUNTED onto the stock +# `postgres` image (no build, no python3). See examples/docker-compose.yml. + +# Honour whatever PGDATA the image uses; default to the classic layout. Stock +# postgres:18+ defaults PGDATA to /var/lib/postgresql/18/docker, so we pin it +# and export it, both for our own appends below and for the upstream scripts. +: "${PGDATA:=/var/lib/postgresql/data}" +export PGDATA + +# Path to the (Python-free) autotune script. Bind-mounted to /autotune.sh by +# default; override with AUTOTUNE_SCRIPT when mounting it elsewhere. +AUTOTUNE_SCRIPT="${AUTOTUNE_SCRIPT:-/autotune.sh}" + +# Zainicjuj bazę danych standardowo (standardowo dla tego obrazu) +/usr/local/bin/docker-ensure-initdb.sh + +# Jeżeli postgresql.conf nie zawiera linii "include_if_exists = /postgresql_optimized.conf" +# to dopisz ją na końcu pliku (idempotentnie): +conf="${PGDATA}/postgresql.conf" +grep -qxF "include_if_exists = '/postgresql_optimized.conf'" "$conf" \ + || echo "include_if_exists = '/postgresql_optimized.conf'" >> "$conf" + +# Wygeneruj /postgresql_optimized.conf (bez Pythona — czysty shell + awk) +sh "$AUTOTUNE_SCRIPT" > /postgresql_optimized.conf + +# Na tym etapie NIE ma potrzeby restartu serwera PostgreSQL, ponieważ zatrzymała go procedura +# stop_tempserver z docker-ensure-initdb/docker-entrypoint. Zatem, wystartuj wszystko normalnie +# z parametrami takimi, jak przekazane: + +exec /usr/local/bin/docker-entrypoint.sh "$@" diff --git a/docker-compose.database.yml b/docker-compose.database.yml index 13dbea0..a41be37 100644 --- a/docker-compose.database.yml +++ b/docker-compose.database.yml @@ -13,27 +13,69 @@ services: dbserver: logging: *default-logging - # Tag iplweb/bpp_dbserver:psql- wybierany przez - # DJANGO_BPP_POSTGRESQL_VERSION w $BPP_CONFIGS_DIR/.env. Dwuwarstwowy - # fallback: nowa nazwa -> stara (DJANGO_BPP_DBSERVER_PG_VERSION, sprzed - # rename w 2026-04-18) -> default 16.13 jako safety net dla deploymentow + # Stock PostgreSQL (oficjalny obraz Debianowy) + autotune bind-mountowany z + # repo (./dbserver/). Obraz iplweb/bpp_dbserver jest WYCOFANY - jego jedynym + # dodatkiem ponad stockowego postgresa byl autotune, ktory teraz montujemy na + # obraz oficjalny (zero buildu, zero python3). Tag wybiera + # DJANGO_BPP_POSTGRESQL_VERSION (MAJOR.MINOR, np. 16.13) w $BPP_CONFIGS_DIR/.env. + # Dwuwarstwowy fallback: nowa nazwa -> stara (DJANGO_BPP_DBSERVER_PG_VERSION, + # sprzed rename 2026-04-18) -> default 16.13 jako safety net dla deploymentow # ktore nie przeszly jeszcze przez init-configs po upgrade repo. Nowe - # konfiguracje dostaja wartosc z init-configs. Pelna lista tagow: - # https://hub.docker.com/r/iplweb/bpp_dbserver/tags - image: iplweb/bpp_dbserver:psql-${DJANGO_BPP_POSTGRESQL_VERSION:-${DJANGO_BPP_DBSERVER_PG_VERSION:-16.13}} + # konfiguracje dostaja wartosc z init-configs. Tagi: https://hub.docker.com/_/postgres + image: postgres:${DJANGO_BPP_POSTGRESQL_VERSION:-${DJANGO_BPP_DBSERVER_PG_VERSION:-16.13}} restart: always + # Service-level env_file wstrzykuje .env do RUNTIME kontenera. UWAGA: + # include-level env_file w docker-compose.yml sluzy TYLKO interpolacji + # compose i NIE trafia do kontenera - dlatego potrzebny jest ten wpis. Daje + # autotune'owi knoby POSTGRESQL_* i jest spojny z sentinelem + # (docker-compose.database.external.yml) oraz backup-runnerem. + env_file: ${BPP_CONFIGS_DIR}/.env + # Wrapper autotune: inicjuje baze (docker-ensure-initdb.sh), generuje + # /postgresql_optimized.conf (czyta limit cgroup ~95%), exec-uje standardowy + # entrypoint. Shebang #!/usr/bin/env bash => obraz Debianowy, NIE -alpine + # (alpine nie ma basha). command: postgres trafia jako "$@" do entrypointu. + entrypoint: ["bash", "/usr/local/bin/docker-entrypoint-autotune.sh"] + command: ["postgres"] environment: POSTGRES_DB: ${DJANGO_BPP_DB_NAME} POSTGRES_USER: ${DJANGO_BPP_DB_USER} POSTGRES_PASSWORD: ${DJANGO_BPP_DB_PASSWORD} + # KRYTYCZNE: stock postgres:18+ domyslnie ustawia PGDATA na + # /var/lib/postgresql//docker. Bez tego pinu istniejacy volume + # zostalby zignorowany, a baza zainicjowana od zera (utrata danych!). + # Mount zostaje na /var/lib/postgresql/data - NIE zmieniaj na + # /var/lib/postgresql mimo nowego VOLUME w obrazie 18+. + PGDATA: /var/lib/postgresql/data + # Stosowane TYLKO przy pustym PGDATA (swieza instalacja): kolacja ICU pl-PL + # dla poprawnego sortowania polskich znakow. Istniejace klastry zachowuja + # swoja oryginalna kolacje - ten arg nigdy nie re-kolacjonuje danych. + POSTGRES_INITDB_ARGS: "--locale-provider=icu --icu-locale=pl-PL" volumes: + # Autotune bind-mountowany z repo (read-only). To wersjonowany kod, nie + # konfiguracja usera - git pull aktualizuje go bez force-sync. Brak chmod +x + # nie szkodzi: skrypty sa wywolywane przez interpreter (bash / sh), nie exec. + - ./dbserver/docker-entrypoint-autotune.sh:/usr/local/bin/docker-entrypoint-autotune.sh:ro + - ./dbserver/autotune.sh:/autotune.sh:ro - postgresql_data:/var/lib/postgresql/data # Katalog na backupy, bind-mount z hosta. Kierujemy tu pg_dump/tar # zamiast pisac do /tmp w writable layer kontenera - inaczej dlugo # zyjacy dbserver by puchnal o rozmiar backupu przy kazdym wywolaniu. - ${DJANGO_BPP_HOST_BACKUP_DIR}:/backup + healthcheck: + # Stock postgres nie ma wbudowanego HEALTHCHECK (dawny obraz iplweb mial go + # w Dockerfile). appserver/authserver maja depends_on: dbserver: + # service_healthy, wiec healthcheck jest WYMAGANY. Uzywamy POSTGRES_USER/ + # POSTGRES_DB (jawnie w environment: powyzej, wiec ZAWSZE obecne, niezaleznie + # od env_file). Podwojny $$ chroni przed ekspansja przez Compose - zmienna + # czytana w kontenerze w runtime, nie w parse-time. + test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""] + interval: 10s + timeout: 5s + retries: 5 + start_period: 60s # Limity zasobow - domyslne wartosci dla 8 GB hosta; `make configure-resources` - # wylicza wartosci proporcjonalne do rzeczywistego rozmiaru hosta. + # wylicza wartosci proporcjonalne do rzeczywistego rozmiaru hosta. Autotune + # czyta ten limit cgroup (memory) i skaluje shared_buffers/work_mem (~95%). deploy: resources: limits: diff --git a/docs/konfiguracja/limity-zasobow.md b/docs/konfiguracja/limity-zasobow.md index fd2bf8c..e2e108e 100644 --- a/docs/konfiguracja/limity-zasobow.md +++ b/docs/konfiguracja/limity-zasobow.md @@ -80,7 +80,16 @@ Razem ≈ **3,3 GB**. Capy te są odejmowane od budżetu, a reszta trafia do us `dbserver` dostaje największą wagę (Postgres najlepiej wykorzystuje RAM na `shared_buffers` i cache stron). `appserver` ma najwyższy floor — gunicorn akumuluje -pamięć (stąd nocny restart `kill 1`). `workerserver` przejął wagę dwóch poprzednich +pamięć (stąd nocny restart `kill 1`). + +!!! info "DBSERVER_MEM_LIMIT steruje strojeniem Postgresa (autotune)" + `dbserver` to stockowy obraz `postgres` z bind-mountowanym skryptem + `dbserver/autotune.sh`, który przy starcie **czyta limit pamięci cgroup** + (`DBSERVER_MEM_LIMIT`) i generuje `/postgresql_optimized.conf` z `shared_buffers`, + `effective_cache_size`, `work_mem`, WAL itd. dobranymi do **~95%** tego limitu. Czyli + `make configure-resources` nie tylko ogranicza kontener — pośrednio stroi też samego + Postgresa. Nie trzeba ręcznie edytować `postgresql.conf`. Szczegóły: + [PostgreSQL](postgresql.md). `workerserver` przejął wagę dwóch poprzednich workerów (20% + 15% = 35%) — patrz [Concurrency Celery](#concurrency-celery) niżej, bo jego realny apetyt na RAM zależy od liczby procesów-dzieci prefork. diff --git a/docs/konfiguracja/postgresql.md b/docs/konfiguracja/postgresql.md index 9ff0126..c3c3552 100644 --- a/docs/konfiguracja/postgresql.md +++ b/docs/konfiguracja/postgresql.md @@ -1,8 +1,17 @@ # PostgreSQL — wersje i upgrade -Kontener `dbserver` używa obrazu `iplweb/bpp_dbserver:psql-${DJANGO_BPP_POSTGRESQL_VERSION}`, -format `MAJOR.MINOR` (np. `16.13`, `17.9`, `18.3`). Wersja jest sterowana zmienną -`DJANGO_BPP_POSTGRESQL_VERSION` w `$BPP_CONFIGS_DIR/.env`. Domyślnie `16.13`. +Kontener `dbserver` używa **oficjalnego obrazu** `postgres:${DJANGO_BPP_POSTGRESQL_VERSION}` +(wariant Debian, nie `-alpine`), format `MAJOR.MINOR` (np. `16.13`, `17.9`, `18.3`). Wersja +jest sterowana zmienną `DJANGO_BPP_POSTGRESQL_VERSION` w `$BPP_CONFIGS_DIR/.env`. Domyślnie +`16.13`. + +> **Skąd autotune?** Wcześniej `dbserver` używał własnego obrazu `iplweb/bpp_dbserver` — +> jest on **wycofany**, a jego jedynym dodatkiem ponad stockowego postgresa był *autotune*. +> Teraz montujemy dwa skrypty autotune (`dbserver/autotune.sh`, +> `dbserver/docker-entrypoint-autotune.sh` — wersjonowane w repo, bind-mount read-only) na +> obraz oficjalny. Wrapper inicjuje bazę, generuje `/postgresql_optimized.conf` dopasowany +> do limitu pamięci kontenera (`DBSERVER_MEM_LIMIT`, ~95%) i startuje normalnie. Bez buildu, +> bez `python3`. Szczegóły strojenia: [Limity zasobów](limity-zasobow.md). `DJANGO_BPP_POSTGRESQL_VERSION_MAJOR` (auto-derived z `_VERSION`) jest używana przez `backup-runner` (`postgres:-alpine` — `pg_dump` musi być ≥ wersji serwera). @@ -10,7 +19,15 @@ W trybie external obie zmienne trzymają tylko major. Wybór wersji następuje przy pierwszym uruchomieniu `make` — `init-configs` zapyta `Wersja PostgreSQL [16.13]:`. Lista tagów: -[hub.docker.com/r/iplweb/bpp_dbserver/tags](https://hub.docker.com/r/iplweb/bpp_dbserver/tags). +[hub.docker.com/_/postgres](https://hub.docker.com/_/postgres). + +!!! note "Kolacja (sortowanie) i PGDATA" + Świeża inicjalizacja bazy używa `POSTGRES_INITDB_ARGS=--locale-provider=icu + --icu-locale=pl-PL` (poprawne sortowanie polskich znaków). Dotyczy to **tylko nowych + instalacji** — istniejące wolumeny zachowują swoją oryginalną kolację, ten argument + nigdy nie re-kolacjonuje danych. `PGDATA` jest przypięte do `/var/lib/postgresql/data`: + stock `postgres:18+` domyślnie używa innej ścieżki, więc bez pinu istniejący wolumen + zostałby zignorowany, a baza zainicjowana od zera. !!! warning Upgrade major wymaga dump/restore — użyj `make upgrade-postgres`, **nie** edytuj @@ -53,8 +70,8 @@ Skrypt (`scripts/upgrade-postgres.sh`) interaktywnie wykonuje kroki: ### Wymagania -- Obraz `iplweb/bpp_dbserver:psql-` już opublikowany na Docker Hub (skrypt - tylko pobiera, nie buduje). +- Obraz `postgres:` — oficjalny obraz Docker, wszystkie majory są zawsze + dostępne (krok 1 robi `docker pull` i wyłapie literówkę w wersji; skrypt nie buduje obrazu). - Wolne miejsce: ~2.5× rozmiar PGDATA (tarball + kopia wolumenu). - Stack musi być uruchomiony (`make up`), żeby wykonać `pg_dump`. diff --git a/docs/superpowers/specs/2026-06-13-dbserver-stock-postgres-autotune-design.md b/docs/superpowers/specs/2026-06-13-dbserver-stock-postgres-autotune-design.md new file mode 100644 index 0000000..48d59dc --- /dev/null +++ b/docs/superpowers/specs/2026-06-13-dbserver-stock-postgres-autotune-design.md @@ -0,0 +1,256 @@ +# Spec: migrate `dbserver` from `iplweb/bpp_dbserver` to stock `postgres` + bind-mounted autotune + +- **Date:** 2026-06-13 +- **Status:** approved design, pre-implementation +- **Scope decision:** full — swap the image **and** simplify the major-upgrade flow +- **Locale decision:** fresh installs initialise with ICU `pl-PL` collation + +## 1. Background & motivation + +The custom image `iplweb/bpp_dbserver:psql-` is **discontinued**. The +`iplweb/bpp-dbserver` GitHub repo no longer contains a `Dockerfile` — it now ships only +the autotune scripts (`autotune.sh`, `autotune.py`, `docker-entrypoint-autotune.sh`) plus +`examples/docker-compose.yml`, which demonstrates running the autotune step on the **stock +`postgres` image** via a bind-mounted entrypoint (no custom build, no `python3`). + +The only thing the discontinued image added on top of stock PostgreSQL was the autotune +step (auto-generated pgtune-style config sized to the container's memory limit). Therefore +this repo migrates the local `dbserver` to the stock `postgres` image with the autotune +scripts bind-mounted on top. + +Today this repo references the discontinued image in: + +- `docker-compose.database.yml` — the real local DB server (the primary target). +- `scripts/upgrade-postgres.sh` — major-version upgrade (dump/restore) flow. +- `scripts/test-upgrade-postgres.sh` — upgrade test harness. +- `scripts/init-configs.sh` — comments/messages and `.env` version-var setup. + +The backup runner (`docker-compose.backup.yml`) and the external-DB sentinel +(`docker-compose.database.external.yml`) already use stock `postgres:-alpine` and are +**out of scope** — they do not run a real DB cluster. + +## 2. Goals / non-goals + +**Goals** + +1. Local `dbserver` runs stock `postgres:` with autotune bind-mounted. +2. Existing `postgresql_data` volumes keep working with **no dump/restore** for the image + swap (same PG major). +3. Fresh installs initialise with ICU `pl-PL` collation. +4. Major-upgrade flow simplified: no "wait for a prebuilt image" prerequisite; upgrade test + runs natively on Apple Silicon. +5. No manual `.env` editing required on `git pull && make up` (backwards-compat contract). + +**Non-goals** + +- Changing the external-DB sentinel or backup-runner images. +- Re-collating existing clusters (ICU `pl-PL` applies to fresh init only). +- Switching the upgrade strategy away from dump/restore (still required across majors). +- Removing the `DJANGO_BPP_POSTGRESQL_VERSION` / `_MAJOR` variables or their migrations. + +## 3. The autotune scripts (copied into this repo) + +Copied **verbatim** from `iplweb/bpp-dbserver@main` (the scripts are "ours"). Two files, +placed in a new repo-root directory `dbserver/`: + +- `dbserver/autotune.sh` — pure `/bin/sh` + `awk` pgtune-style generator. **cgroup-aware**: + reads `/sys/fs/cgroup/memory.max` (v2) or `memory.limit_in_bytes` (v1), falls back to + `/proc/meminfo`, and sizes `shared_buffers` (RAM/4), `effective_cache_size` (3·RAM/4), + `work_mem`, `maintenance_work_mem`, WAL, parallelism, etc. Uses **95%** of the detected + limit by default. Has a `--test` self-check (byte-parity with the Python original). +- `dbserver/docker-entrypoint-autotune.sh` — `#!/usr/bin/env bash` wrapper: + 1. defaults & exports `PGDATA` (`/var/lib/postgresql/data`), + 2. runs `/usr/local/bin/docker-ensure-initdb.sh` (standard image initdb), + 3. idempotently appends `include_if_exists = '/postgresql_optimized.conf'` to + `$PGDATA/postgresql.conf`, + 4. generates `/postgresql_optimized.conf` by running `$AUTOTUNE_SCRIPT` (default + `/autotune.sh`), + 5. `exec /usr/local/bin/docker-entrypoint.sh "$@"`. + +**Delivery mechanism: bind-mount from the repo working tree — NOT force-sync into +`$BPP_CONFIGS_DIR`.** Rationale: these are versioned code, not user-tunable config, so a +`git pull` keeps them current with zero machinery. This mirrors the existing pattern where +`backup-runner` bind-mounts `./scripts:/scripts:ro`. Compose resolves the relative paths +against the compose-file directory (the repo), independent of CWD. **No `chmod +x` needed:** +neither script is exec'd directly — the entrypoint is invoked as `["bash", "...autotune.sh"]` +and the wrapper runs autotune via `sh "$AUTOTUNE_SCRIPT"` — so a missing exec bit on the `:ro` +mount is irrelevant. + +**Autotune env knobs** (all optional; read from the container's *runtime* environment). To +make these reach the script, the `dbserver` service gets its own **service-level** +`env_file: ${BPP_CONFIGS_DIR}/.env` (see §4). The `include`-level `env_file` in +`docker-compose.yml` only supplies variables for **compose-file interpolation** and is **not** +injected into containers — so a service-level `env_file` is required (this is exactly how the +external sentinel at `docker-compose.database.external.yml:39` and `backup-runner` already +work). `compose` applies `environment:` over `env_file`, so our explicit `POSTGRES_*`/`PGDATA`/ +`POSTGRES_INITDB_ARGS` keys always win. None of the autotune knobs are currently set anywhere +in the repo, so defaults apply: +`POSTGRESQL_RAM_PERCENT` (0.95), `POSTGRESQL_RAM_THIS_MUCH_GB`, `POSTGRESQL_DEFAULT_RAM` +(4096), `POSTGRESQL_UNSAFE_BUT_FAST`, `POSTGRESQL_MAX_LOCKS_PER_TRANSACTION`, +`POSTGRESQL_MAX_PRED_LOCKS_PER_TRANSACTION`. + +## 4. `docker-compose.database.yml` changes + +Target `dbserver` service: + +```yaml +dbserver: + logging: *default-logging + # Stock PostgreSQL (obraz oficjalny, Debian) + autotune bind-mountowany z repo. + # Tag wybiera DJANGO_BPP_POSTGRESQL_VERSION (MAJOR.MINOR). Dwuwarstwowy fallback: + # nowa nazwa -> stara (DJANGO_BPP_DBSERVER_PG_VERSION) -> default 16.13. + # Tagi: https://hub.docker.com/_/postgres + image: postgres:${DJANGO_BPP_POSTGRESQL_VERSION:-${DJANGO_BPP_DBSERVER_PG_VERSION:-16.13}} + restart: always + # Service-level env_file: wstrzykuje .env do RUNTIME kontenera (include-level + # env_file sluzy tylko interpolacji compose, NIE trafia do kontenera). Daje to + # autotune'owi knoby POSTGRESQL_* i jest spojne z sentinelem/backup-runnerem. + env_file: ${BPP_CONFIGS_DIR}/.env + # bash + docker-ensure-initdb.sh => obraz Debianowy (NIE -alpine). + entrypoint: ["bash", "/usr/local/bin/docker-entrypoint-autotune.sh"] + command: ["postgres"] + environment: + POSTGRES_DB: ${DJANGO_BPP_DB_NAME} + POSTGRES_USER: ${DJANGO_BPP_DB_USER} + POSTGRES_PASSWORD: ${DJANGO_BPP_DB_PASSWORD} + # KRYTYCZNE: stock postgres:18+ domyslnie ma PGDATA=/var/lib/postgresql/18/docker. + # Bez tego pinu istniejacy volume zostalby zignorowany i baza zainicjowana od zera. + PGDATA: /var/lib/postgresql/data + # Tylko przy PUSTYM PGDATA (fresh install): kolacja ICU pl-PL dla polskiego sortowania. + POSTGRES_INITDB_ARGS: "--locale-provider=icu --icu-locale=pl-PL" + volumes: + - ./dbserver/docker-entrypoint-autotune.sh:/usr/local/bin/docker-entrypoint-autotune.sh:ro + - ./dbserver/autotune.sh:/autotune.sh:ro + - postgresql_data:/var/lib/postgresql/data + - ${DJANGO_BPP_HOST_BACKUP_DIR}:/backup + healthcheck: + # Stock postgres nie ma wbudowanego healthchecku (dawny obraz mial HEALTHCHECK + # w Dockerfile). appserver/authserver maja depends_on: dbserver: service_healthy, + # wiec healthcheck jest WYMAGANY. Podwojny $$ zapobiega ekspansji przez Compose. + # Uzywamy POSTGRES_USER/POSTGRES_DB (ustawione jawnie w environment: wyzej, wiec + # ZAWSZE obecne) zamiast DJANGO_BPP_DB_* - nie zalezy od env_file. + test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""] + interval: 10s + timeout: 5s + retries: 5 + start_period: 60s + deploy: + resources: + limits: + memory: ${DBSERVER_MEM_LIMIT:-2g} + cpus: "${DBSERVER_CPU_LIMIT:-2.0}" +``` + +Unchanged: the `postgresql_data` volume definition, the `${DJANGO_BPP_HOST_BACKUP_DIR}` +bind, the resource limits (autotune reads this cgroup memory limit), the `logging` anchor. + +`pg_isready` with no `-h` connects via the local socket inside the container; this verifies +the cluster (not merely the port). `start_period: 60s` covers cold start and first-init. + +### Decisions baked in (flagged earlier, no further input requested) + +- Scripts live in **`dbserver/`** at repo root (vs `scripts/dbserver/`). +- Healthcheck cadence: `interval 10s / timeout 5s / retries 5 / start_period 60s`. +- `POSTGRES_INITDB_ARGS` mirrors the upstream example **verbatim** + (`--locale-provider=icu --icu-locale=pl-PL`); no explicit `--encoding`/`--locale` — the + stock image's `LANG=en_US.utf8` supplies a UTF-8 `LC_CTYPE` and UTF8 encoding, which is + ICU-compatible. +- Healthcheck uses `$$POSTGRES_USER`/`$$POSTGRES_DB` (set in `environment:`), not + `$$DJANGO_BPP_DB_*` — guaranteed present and independent of `env_file`. +- **PG18 caveat:** `postgres:18+` relocated its declared `VOLUME` from + `/var/lib/postgresql/data` to `/var/lib/postgresql`. We deliberately keep mounting + `postgresql_data` at `/var/lib/postgresql/data` **and** pin `PGDATA` there (matching the + existing volume and the upstream example). Do **not** "modernise" the mount to + `/var/lib/postgresql` — that would orphan every existing cluster. + +## 5. Backwards compatibility + +- **No `.env` migration needed.** `DJANGO_BPP_POSTGRESQL_VERSION` (e.g. `16.13`) already maps + directly to a valid stock tag `postgres:16.13`. The two-tier fallback to + `DJANGO_BPP_DBSERVER_PG_VERSION` is preserved in the compose `image:` line. +- **Existing volumes** start unchanged: same PG major, and `PGDATA` is pinned to the existing + mount path, so stock postgres reads the existing cluster (the old image was stock postgres + + autotune). No dump/restore for the swap. +- **Accepted divergence:** fresh installs collate Polish via ICU `pl-PL`; pre-existing volumes + keep their original collation. `POSTGRES_INITDB_ARGS` never re-collates an existing cluster. + +## 6. Upgrade flow simplification + +`scripts/upgrade-postgres.sh`: + +- `NEW_DBSERVER_IMAGE` (currently `iplweb/bpp_dbserver:psql-${NEW_POSTGRESQL_VERSION}`, ~line + 552) → `postgres:${NEW_POSTGRESQL_VERSION}`. The `docker pull` (~line 679) still works. +- Remove the "upstream image with the new major MUST already be published" prerequisite and + the `hub.docker.com/r/iplweb/bpp_dbserver/tags` messaging (~lines 27, 507). Stock postgres + always publishes every major. +- Dump → recreate volume → restore across majors is **unchanged** (still required). + +`scripts/test-upgrade-postgres.sh`: + +- Drop **both** the inline `platform: linux/amd64` key + comment (~lines 86-89) **and** the + `DOCKER_DEFAULT_PLATFORM=linux/amd64` export block (~lines 121-130). Removing only the + inline key would leave the `export` still force-pulling amd64 under qemu — it affects both + `docker pull` and `docker compose`. Stock postgres is multi-arch → native on Apple Silicon. +- Point the harness at `postgres:${DJANGO_BPP_POSTGRESQL_VERSION:-16.13}` (~line 89) and + **mirror the production service**: service-level `env_file: ${BPP_CONFIGS_DIR}/.env`, + bind-mount the autotune scripts, set `entrypoint`/`command`, pin `PGDATA`, and add the + `pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"` healthcheck — same C1 fix as §4, so the + test reflects the real deployment. The healthcheck here is **mandatory, not cosmetic**: + `wait_for_healthy` (~lines 152-171) polls `.State.Health.Status`, and stock postgres has no + baked-in `HEALTHCHECK` (the old image did) — without it the test loops until timeout. + +## 7. Touch-ups + +- `scripts/init-configs.sh`: reword comments/messages mentioning + `iplweb/bpp_dbserver:psql-` and the hub.docker.com tags page (~lines 301-302, 421-435, + 756-760) to stock-`postgres` wording. **The version-variable logic and the old-name + migrations stay exactly as they are** (`DJANGO_BPP_POSTGRESQL_VERSION` = MAJOR.MINOR, + `_MAJOR` derived; migrations from `DJANGO_BPP_DBSERVER_PG_VERSION` / + `DJANGO_BPP_POSTGRESQL_DB_VERSION` preserved). +- `scripts/check-image-versions.sh`: this script skips `iplweb/*` images. After the swap, + `postgres:` is no longer skipped, so it joins the version report. That is desirable for + minor updates but will also surface major bumps (e.g. `16.x → 18.x`) that actually require + `make upgrade-postgres`. Add a one-line comment noting this; **no logic change.** + +## 8. Documentation (via the `docs-sync` skill) + +- `docs/konfiguracja/postgresql.md` — image source, autotune mechanism, simplified upgrade + flow. +- `docs/konfiguracja/limity-zasobow.md` — note that the DB autotune reads the `DBSERVER_MEM_LIMIT` + cgroup limit (~95%), so `make configure-resources` drives PostgreSQL tuning. +- `CLAUDE.md` — "PostgreSQL version vars" and "Running commands in containers" sections: + replace the `iplweb/bpp_dbserver` references; document the bind-mounted autotune + `dbserver/` + dir + the force-sync exception (these scripts are bind-mounted, not force-synced). +- `README.md` — only if it names the image. +- Run `mkdocs build --strict` afterwards. + +## 9. Verification + +1. `docker compose config` validates (interpolation + merged service). +2. `bash dbserver/autotune.sh --test` passes (built-in parity self-test). +3. Bring `dbserver` up on a copy of an existing-style `postgresql_data` volume: starts + `healthy`, `/postgresql_optimized.conf` is generated, `SHOW shared_buffers;` tracks + `DBSERVER_MEM_LIMIT` (≈ limit/4). +4. Fresh-init smoke test: empty volume → `SHOW lc_collate;` / `datlocprovider` reflects ICU + `pl-PL`. +5. `appserver`/`authserver` reach `service_healthy` gate (depend on `dbserver` healthy). +6. Existing CI green: Makefile tests (Linux/macOS/Windows) + docker-compose validation. +7. Upgrade path: `scripts/test-upgrade-postgres.sh` runs on Apple Silicon without + `platform: linux/amd64`. + +## 10. Risks & mitigations + +| Risk | Mitigation | +|---|---| +| `postgres:18+` re-inits a blank cluster on an existing volume | Pin `PGDATA=/var/lib/postgresql/data` (matches existing mount). | +| Missing healthcheck → `appserver`/`authserver` never start | Add `pg_isready` healthcheck (§4). | +| Healthcheck/autotune vars silently empty — `include`-level `env_file` is interpolation-only, not injected into the container | Service-level `env_file` on `dbserver`; healthcheck uses the explicit `environment:` keys `$$POSTGRES_USER`/`$$POSTGRES_DB` (§3/§4). | +| Using `-alpine` (no bash) breaks the entrypoint | Use the Debian-based `postgres:` tag explicitly. | +| ICU locale id invalid on the chosen major | `--icu-locale` valid on PG 15–18; default `16.13` is in range. | +| `check-image-versions.sh` suggests a major jump as "latest" | Documented comment; major bumps go through `make upgrade-postgres`. | + +## 11. Out of scope / follow-ups + +- Removing the discontinued image's old tags from anyone's local cache (they still exist on + Docker Hub; existing installs keep working until they `git pull`). +- Stray repo-root artifacts (`#docker-compose.database.yml#`, `REVIEW.md~`) — unrelated. diff --git a/scripts/check-image-versions.sh b/scripts/check-image-versions.sh index f9fb030..4aba91c 100755 --- a/scripts/check-image-versions.sh +++ b/scripts/check-image-versions.sh @@ -12,7 +12,10 @@ set -euo pipefail REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" -# Wyciagnij wszystkie nieblokowane obrazy (pomijamy iplweb/* bo to nasze wlasne) +# Wyciagnij wszystkie nieblokowane obrazy (pomijamy iplweb/* bo to nasze wlasne). +# UWAGA: dbserver to teraz stockowy postgres:, wiec pojawi sie tutaj. +# Skrypt pokaze najnowszy tag - dla postgresa moze to byc inny MAJOR (np. 16->18). +# Minor bump (16.13->16.14) zrob recznie; major bump idzie przez 'make upgrade-postgres'. mapfile -t IMAGES < <( grep -h 'image:' "$REPO_DIR"/docker-compose.*.yml \ | sed 's/.*image: *//' \ diff --git a/scripts/init-configs.sh b/scripts/init-configs.sh index dfe19b0..2297ab1 100755 --- a/scripts/init-configs.sh +++ b/scripts/init-configs.sh @@ -298,8 +298,8 @@ if [ ! -f "$ENV_FILE" ]; then echo "" echo "=== Wersja PostgreSQL dla dbserver ===" - echo "Kontener dbserver uzywa obrazu iplweb/bpp_dbserver:psql-." - echo "Dostepne tagi: https://hub.docker.com/r/iplweb/bpp_dbserver/tags" + echo "Kontener dbserver uzywa oficjalnego obrazu postgres: + autotune." + echo "Dostepne tagi: https://hub.docker.com/_/postgres" echo "Przyklady: 16.13, 17.9, 18.3 (zalecany format MAJOR.MINOR)." printf "Wersja PostgreSQL [16.13]: " read -r DBSERVER_PG_VERSION || true @@ -418,7 +418,7 @@ EOF fi # Zmienne wersji dla trybu lokalnego: - # DJANGO_BPP_POSTGRESQL_VERSION - pelny MAJOR.MINOR, tag iplweb/bpp_dbserver:psql- + # DJANGO_BPP_POSTGRESQL_VERSION - pelny MAJOR.MINOR, tag postgres: # DJANGO_BPP_POSTGRESQL_VERSION_MAJOR - derived major, dla backup-runnera (postgres:-alpine) # Upgrade majora: `make upgrade-postgres` (logical dump & restore z zachowaniem # starego volume jako kopii zapasowej). @@ -427,7 +427,7 @@ EOF cat >> "$ENV_FILE" <). +# DJANGO_BPP_POSTGRESQL_VERSION - pelny tag dbservera (postgres:). # Nie zmieniaj recznie dla upgrade'u majora - uzyj 'make upgrade-postgres' # (dump & restore). Minor update (np. 16.13 -> 16.14) mozna zrobic recznie. DJANGO_BPP_POSTGRESQL_VERSION=$DBSERVER_PG_VERSION @@ -753,11 +753,11 @@ else # W trybie lokalnej bazy - zapytaj o wersje dbservera i backup-runnera. if [ "$BPP_EXTERNAL_DB" != "yes" ]; then - # Tag iplweb/bpp_dbserver:psql-. Default 16.13 to ostatnio - # znana dobra wersja - dla starych deploymentow po `git pull` daje - # zgodnosc bit-in-bit z poprzednim hardcoded tagem z docker-compose. + # Tag postgres:. Default 16.13 to ostatnio znana dobra + # wersja - dla starych deploymentow po `git pull` daje zgodnosc + # bit-in-bit z poprzednim hardcoded tagem z docker-compose. ensure_env_var "DJANGO_BPP_POSTGRESQL_VERSION" "16.13" \ - "Wersja dbservera (iplweb/bpp_dbserver:psql-, np. 16.13, 17.9, 18.3)" \ + "Wersja dbservera (postgres:, np. 16.13, 17.9, 18.3)" \ "Wersja PostgreSQL - upgrade majora przez 'make upgrade-postgres'" # Default dla backup-runnera = major z dbservera (16.13 -> 16) tylko diff --git a/scripts/test-upgrade-postgres.sh b/scripts/test-upgrade-postgres.sh index 6919891..90cfeea 100755 --- a/scripts/test-upgrade-postgres.sh +++ b/scripts/test-upgrade-postgres.sh @@ -2,7 +2,7 @@ # # Integration test dla scripts/upgrade-postgres.sh. # -# Scenariusz: upgrade iplweb/bpp_dbserver:psql-16.13 -> :psql-18.3. +# Scenariusz: upgrade postgres:16.13 -> postgres:18.3 (stock obraz + autotune). # # Test buduje izolowana piaskownice: # - wlasny COMPOSE_PROJECT_NAME (unikalny per run) @@ -83,17 +83,33 @@ trap cleanup EXIT INT TERM cat > "$TEST_COMPOSE" <<'COMPOSE_EOF' services: dbserver: - # iplweb/bpp_dbserver jest zbudowany tylko dla linux/amd64 - na ARM Mac - # bez platform: musieli bysmy liczyc na auto qemu, ktorego nie ma. - platform: linux/amd64 - image: iplweb/bpp_dbserver:psql-${DJANGO_BPP_POSTGRESQL_VERSION:-16.13} + # Stock postgres jest multi-arch (amd64 + arm64) => brak platform: i brak + # DOCKER_DEFAULT_PLATFORM, test leci natywnie na ARM Mac. Mirror produkcji + # (docker-compose.database.yml): autotune bind-mount z repo (${DBSERVER_DIR}), + # PGDATA pin, healthcheck. Healthcheck jest WYMAGANY - wait_for_healthy w + # upgrade-postgres.sh czyta .State.Health.Status, a stock postgres nie ma + # wbudowanego HEALTHCHECK (dawny obraz iplweb mial go w Dockerfile). + image: postgres:${DJANGO_BPP_POSTGRESQL_VERSION:-16.13} + env_file: ${BPP_CONFIGS_DIR}/.env + entrypoint: ["bash", "/usr/local/bin/docker-entrypoint-autotune.sh"] + command: ["postgres"] environment: POSTGRES_DB: ${DJANGO_BPP_DB_NAME} POSTGRES_USER: ${DJANGO_BPP_DB_USER} POSTGRES_PASSWORD: ${DJANGO_BPP_DB_PASSWORD} + PGDATA: /var/lib/postgresql/data + POSTGRES_INITDB_ARGS: "--locale-provider=icu --icu-locale=pl-PL" volumes: + - ${DBSERVER_DIR}/docker-entrypoint-autotune.sh:/usr/local/bin/docker-entrypoint-autotune.sh:ro + - ${DBSERVER_DIR}/autotune.sh:/autotune.sh:ro - postgresql_data:/var/lib/postgresql/data - ${DJANGO_BPP_HOST_BACKUP_DIR}:/backup + healthcheck: + test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""] + interval: 10s + timeout: 5s + retries: 5 + start_period: 60s volumes: postgresql_data: @@ -118,16 +134,12 @@ EOF export BPP_CONFIGS_DIR="$TEST_CONFIGS" export COMPOSE_PROJECT_NAME="$TEST_PROJECT" export COMPOSE_FILE="$TEST_COMPOSE" -# iplweb/bpp_dbserver jest dostarczany tylko jako linux/amd64. Na ARM Mac -# (M1/M2/M3) musimy to powiedziec Dockerowi explicite. Skrypt w kroku 1 robi -# `docker pull ` bez --platform; DOCKER_DEFAULT_PLATFORM dziala na -# docker pull i docker compose, wiec jedna zmienna zalatwia oba. -if [ -z "${DOCKER_DEFAULT_PLATFORM:-}" ]; then - _arch="$(uname -m)" - case "$_arch" in - arm64|aarch64) export DOCKER_DEFAULT_PLATFORM=linux/amd64 ;; - esac -fi +# Autotune bind-mountowany z repo - absolutna sciezka, bo compose rozwiazuje +# wzgledne sciezki wzgledem katalogu pliku compose (tu: $TEST_ROOT, nie repo). +# Eksport => upgrade-postgres.sh (proces potomny) zobaczy ja przy docker compose. +export DBSERVER_DIR="$REPO_DIR/dbserver" +# Stock postgres jest multi-arch => NIE ustawiamy DOCKER_DEFAULT_PLATFORM (dawny +# iplweb/bpp_dbserver byl tylko linux/amd64 i wymagal tego na ARM Mac). set -a # shellcheck disable=SC1090 . "$TEST_ENV" diff --git a/scripts/upgrade-postgres.sh b/scripts/upgrade-postgres.sh index b620801..c3ab2e4 100755 --- a/scripts/upgrade-postgres.sh +++ b/scripts/upgrade-postgres.sh @@ -11,10 +11,10 @@ # 6. pg_restore -Fd -j N z tarballa # 7. make migrate, make up, smoke test # -# Wybor dump & restore zamiast pg_upgrade jest swiadomy: obraz iplweb/bpp_dbserver -# ma tylko jeden major Postgresa baked in, wiec pg_upgrade in-place wymagalby -# dedykowanego upgrade-image z dwiema binariami. Logical dump wykorzystuje -# istniejacy `make db-backup` i jest forward-compatible przez wiele majorow. +# Wybor dump & restore zamiast pg_upgrade jest swiadomy: kazdy obraz postgres: +# ma tylko jeden major Postgresa, wiec pg_upgrade in-place wymagalby dedykowanego +# obrazu z dwiema binariami. Logical dump wykorzystuje istniejacy `make db-backup` +# i jest forward-compatible przez wiele majorow. # # Tryb external (BPP_DATABASE_COMPOSE=docker-compose.database.external.yml) ma # wlasna, drastycznie prostsza sciezke - upgrade prawdziwej bazy robi admin po @@ -24,8 +24,10 @@ # - Docker Compose v2.20+ (juz wymagane przez include w docker-compose.yml) # - Wystarczajaco miejsca na hoscie na: tarball pg_dump + drugi volume z kopia # starego PGDATA. Dla DB rozmiaru X potrzeba ~2.5 * X wolnego miejsca. -# - Upstream image iplweb/bpp_dbserver: z nowym majorem MUSI byc juz -# wypchniety na Docker Hub. Skrypt nie buduje obrazu. +# - Obraz postgres: z nowym majorem - oficjalny obraz Docker, wszystkie +# majory sa zawsze dostepne (nie trzeba juz czekac na publikacje wlasnego +# obrazu). Krok 1 robi pre-flight `docker pull` i wylapie literowke w wersji. +# Skrypt nie buduje obrazu. # # Wywolanie: `make upgrade-postgres` lub bezposrednio `bash scripts/upgrade-postgres.sh`. # @@ -504,7 +506,7 @@ EOF echo "BLAD: --noinput wymaga --new-version=X.Y (skad wziac docelowa wersje?)." >&2 exit 1 else - echo "Dostepne tagi (psql-): https://hub.docker.com/r/iplweb/bpp_dbserver/tags" + echo "Dostepne tagi: https://hub.docker.com/_/postgres" read -r -p "Nowa wersja dbservera (format MAJOR.MINOR, np. 18.3): " NEW_POSTGRESQL_VERSION fi if [ -z "$NEW_POSTGRESQL_VERSION" ]; then @@ -549,7 +551,7 @@ EOF echo "Plik stanu: $ROLLBACK_FILE" fi -NEW_DBSERVER_IMAGE="iplweb/bpp_dbserver:psql-${NEW_POSTGRESQL_VERSION}" +NEW_DBSERVER_IMAGE="postgres:${NEW_POSTGRESQL_VERSION}" # Trap z duzym banerem na wypadek awarii po krytycznym kroku (gdy # auto_rollback nie zostal wywolany - np. przerwanie Ctrl-C, blad w obcym From 019c1fb20a8b403494f2e5a0ddb9f06e0b58cce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sat, 13 Jun 2026 20:34:05 +0200 Subject: [PATCH 02/15] feat(dbserver): scripts to migrate an existing DB off the libc pl_PL collation Clusters created by the discontinued iplweb/bpp_dbserver image bake lc_messages/lc_monetary/lc_numeric/lc_time and datcollate/datctype = pl_PL.utf-8, so they cannot start on the stock postgres image (which lacks the pl_PL.UTF-8 libc locale). Migrating such a cluster therefore requires dump -> strip the pl_PL collation -> restore into a fresh stock cluster (ICU pl-PL), not an in-place image swap. Adds a 3-step toolset (+ shared lib, make targets, runbook): - pg-collation-migrate-1-dump.sh : pg_dump -Fd of the live (old-image) cluster - pg-collation-migrate-2-fix.sh : pg_restore -f - | strip CREATE/ALTER/COMMENT COLLATION pl_PL + COLLATE "pl_PL" -> .sql.gz - pg-collation-migrate-3-load.sh : dropdb/createdb (ICU) + psql -f into psql 18 - mk/database.mk: migrate-collation-{dump,fix,load} targets - docs/eksploatacja/migracja-collation-stock-pg.md: runbook Pairs with bpp migration 0443_drop_pl_PL_collation. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../migracja-collation-stock-pg.md | 127 ++++++++++++++++++ mk/database.mk | 42 +++++- scripts/lib-pg-collation-migrate.sh | 119 ++++++++++++++++ scripts/pg-collation-migrate-1-dump.sh | 71 ++++++++++ scripts/pg-collation-migrate-2-fix.sh | 85 ++++++++++++ scripts/pg-collation-migrate-3-load.sh | 106 +++++++++++++++ 6 files changed, 549 insertions(+), 1 deletion(-) create mode 100644 docs/eksploatacja/migracja-collation-stock-pg.md create mode 100644 scripts/lib-pg-collation-migrate.sh create mode 100755 scripts/pg-collation-migrate-1-dump.sh create mode 100755 scripts/pg-collation-migrate-2-fix.sh create mode 100755 scripts/pg-collation-migrate-3-load.sh diff --git a/docs/eksploatacja/migracja-collation-stock-pg.md b/docs/eksploatacja/migracja-collation-stock-pg.md new file mode 100644 index 0000000..ddfa2b0 --- /dev/null +++ b/docs/eksploatacja/migracja-collation-stock-pg.md @@ -0,0 +1,127 @@ +# Migracja bazy na stockowy postgres — pozbycie się kolacji libc `pl_PL` + +Przy przejściu z obrazu `iplweb/bpp_dbserver` na **stockowy `postgres`** (np. +przy okazji upgrade'u majora do 18) istniejącego klastra **nie da się przełączyć +w miejscu**. Trzeba zrobić logical dump → usunąć kolację → załadować do świeżego +klastra. Ten dokument opisuje gotowy 3-krokowy zestaw skryptów. + +## Dlaczego w ogóle + +Stary obraz `iplweb/bpp_dbserver` dorzucał ponad stockowego postgresa **dwie** +rzeczy: autotune **oraz** wygenerowane locale libc `pl_PL.UTF-8` +(`RUN localedef … pl_PL.UTF-8` + `ENV LANG=pl_PL.utf-8`). Oficjalny obraz +`postgres` ma tylko `en_US.UTF-8` i `C.UTF-8`. Konsekwencje: + +1. **Istniejący klaster nie wstanie** na stockowym obrazie — `postgresql.conf` + ma `lc_messages/lc_monetary/lc_numeric/lc_time = pl_PL.utf-8`, a + `pg_database.datcollate/datctype = pl_PL.utf-8`. Stock postgres rzuca + `FATAL: … invalid value for parameter "lc_messages": "pl_PL.utf-8"`. +2. **Zrzut z `CREATE COLLATION … libc pl_PL.UTF-8`** (migracja bpp + `0001_collation`) nie wczyta się na czystym obrazie. + +Kolacja `pl_PL` była używana wyłącznie na **stałych literałach ASCII** +(`'bpp_patent'::text COLLATE "pl_PL"`) w 5 widokach `bpp_kronika_*_view` i +propagowała się do `bpp_kronika_all_unsorted_view` → `bpp_kronika_view`. Dla +sortowania to **no-op**, więc usuwamy ją bezpiecznie. W kodzie BPP robi to +migracja `0443_drop_pl_PL_collation`; tutaj usuwamy ją ze **zrzutu**. + +## Wymagania wstępne + +- **Wdrożona wersja aplikacji z migracją `0442_drop_plpython3u`** (i najlepiej + `0443_drop_pl_PL_collation`). Bez 0442 zrzut nadal zawiera `plpython3u`, + którego stock postgres nie ma — load padnie. Skrypt kroku 2 **ostrzeże**, + jeśli wykryje plpython. +- Docelowa wersja Postgresa ustawiona w `$BPP_CONFIGS_DIR/.env` + (`DJANGO_BPP_POSTGRESQL_VERSION=18.x`) i `docker-compose.database.yml` + używający stockowego `postgres` z + `POSTGRES_INITDB_ARGS=--locale-provider=icu --icu-locale=pl-PL`. +- Działający Docker + miejsce na zrzut. + +## Procedura + +> Kolejność jest istotna: **najpierw** zrzuć ze starego (działającego) klastra, +> **potem** wymień obraz na stock. Stary klaster ma locale libc, więc i dump, i +> ewentualna migracja 0443 wykonają się bez problemu. + +### 0. Zatrzymaj zapisy + +```bash +docker compose stop appserver workerserver celerybeat denorm-queue +``` + +(albo użyj `--stop-app` w kroku 1). + +### 1. Zrzut bieżącego klastra (stary obraz) + +```bash +make migrate-collation-dump +# lub: bash scripts/pg-collation-migrate-1-dump.sh --stop-app +``` + +Wypisze ścieżkę do tarballa, np. +`/…/backups/db-backup-20260613-190000.tar.gz` (format katalogowy `pg_dump -Fd`, +tak jak `make db-backup`). + +### 2. Usuń kolację `pl_PL` ze zrzutu + +```bash +make migrate-collation-fix TARBALL=/…/backups/db-backup-20260613-190000.tar.gz +# lub: bash scripts/pg-collation-migrate-2-fix.sh +``` + +Konwertuje zrzut katalogowy na czysty SQL (`pg_restore -f -`), wycina +`CREATE/ALTER/COMMENT … COLLATION … "pl_PL"` oraz klauzule `COLLATE "pl_PL"`, +i zapisuje `…-nocollation.sql.gz`. Weryfikuje brak pozostałości i ostrzega o +plpython. Jest **idempotentny** — jeśli zrzut był już zrobiony po migracji +0443 (bez kolacji), po prostu nic nie znajdzie. + +### 3. Załaduj do świeżego klastra psql 18 + +Najpierw postaw **świeży** klaster na nowym obrazie. Albo przez istniejący +`make upgrade-postgres`, albo pozwól zrobić to skryptowi (`--recreate-volume`): + +```bash +make migrate-collation-load SQLGZ=/…/backups/db-backup-20260613-190000-nocollation.sql.gz RECREATE=1 +# lub: bash scripts/pg-collation-migrate-3-load.sh <…-nocollation.sql.gz> --recreate-volume +``` + +`--recreate-volume` zatrzyma aplikację + dbserver, **usunie** volume +`${COMPOSE_PROJECT_NAME}_postgresql_data` (DESTRUKCYJNE — pyta o potwierdzenie!), +wstanie dbserver na nowym obrazie (initdb z ICU pl-PL), po czym `dropdb`+ +`createdb`+`psql -f`. Bez `--recreate-volume` zakłada, że dbserver już chodzi na +stockowym obrazie na pustym volume. + +### 4. Domigruj i wstań + +```bash +make migrate # zastosuje 0443 (no-op, kolacji już nie ma) i resztę +make up +``` + +## Weryfikacja + +```bash +# Brak kolacji w schemacie publicznym (oczekiwane: 0): +make dbshell-psql # potem: +# SELECT count(*) FROM pg_collation c JOIN pg_namespace n ON n.oid=c.collnamespace +# WHERE c.collname='pl_PL' AND n.nspname='public'; +# Widoki kroniki działają: +# SELECT count(*) FROM bpp_kronika_view; +# Baza jest ICU: +# SELECT datlocprovider FROM pg_database WHERE datname=current_database(); -- 'i' +``` + +## Uwagi i ograniczenia + +- **Load jest jednowątkowy** (`psql -f`), bo wycinamy kolację na poziomie + czystego SQL (format katalogowy trzyma DDL w binarnym `toc.dat`, więc + `pg_restore -L` nie wytnie klauzul `COLLATE` z definicji widoków). Dla bardzo + dużych baz to wolniejsze niż `pg_restore -Fd -j`, ale to operacja jednorazowa. +- **`make backup` wciąż działa ze starym obrazem** — obrazy `iplweb/bpp_dbserver` + są nadal na Docker Hubie, więc dump da się zrobić nawet po decyzji o migracji. +- Skrypt kroku 3 **odmówi** załadowania do kontenera nadal działającego na + `iplweb/bpp_dbserver` (ładowałbyś do starego klastra, nie do psql 18). +- Pozostała po migracji systemowa kolacja `pg_catalog.pl_PL` (auto-import z + `locale -a` przy initdb) jest nieszkodliwa — `pg_dump` jej nie zrzuca, a na + stockowym obrazie i tak nie powstaje. +``` diff --git a/mk/database.mk b/mk/database.mk index 32bc64c..b337bb1 100644 --- a/mk/database.mk +++ b/mk/database.mk @@ -4,7 +4,8 @@ restore-db-stop-servers restore-db-remove-db-rebuild-db-rm-backup \ restore-remote-db-from-dump restore-remote-db-from-dump-dont-backup \ upgrade-postgres test-upgrade-postgres \ - push-local-bpp-db-to-remote + push-local-bpp-db-to-remote \ + migrate-collation-dump migrate-collation-fix migrate-collation-load # Katalog backupow na hoscie. Nowa nazwa: DJANGO_BPP_HOST_BACKUP_DIR # (stara: DJANGO_BPP_BACKUP_DIR - fallback dla deploymentow ktore jeszcze @@ -174,3 +175,42 @@ upgrade-postgres: # obrazy (psql-16.13 i psql-18.3) sa pullowane przez test + skrypt. test-upgrade-postgres: @bash scripts/test-upgrade-postgres.sh + +# Migracja kolacji libc pl_PL -> stockowy postgres. Trzy kroki (thin wrappery +# na scripts/pg-collation-migrate-{1-dump,2-fix,3-load}.sh). Pelny opis: +# docs/eksploatacja/migracja-collation-stock-pg.md. +# make migrate-collation-dump [STOP_APP=1] [YES=1] +# make migrate-collation-fix TARBALL=/.../db-backup-*.tar.gz +# make migrate-collation-load SQLGZ=/.../*-nocollation.sql.gz [RECREATE=1] [YES=1] +COLLATION_DUMP_FLAGS := +ifdef STOP_APP + COLLATION_DUMP_FLAGS += --stop-app +endif +ifdef YES + COLLATION_DUMP_FLAGS += --yes +endif + +migrate-collation-dump: + @bash scripts/pg-collation-migrate-1-dump.sh $(COLLATION_DUMP_FLAGS) + +migrate-collation-fix: + @if [ -z "$(TARBALL)" ]; then \ + echo "Uzycie: make migrate-collation-fix TARBALL=/.../db-backup-YYYYMMDD-HHMMSS.tar.gz" >&2; \ + exit 1; \ + fi + @bash scripts/pg-collation-migrate-2-fix.sh "$(TARBALL)" + +COLLATION_LOAD_FLAGS := +ifdef RECREATE + COLLATION_LOAD_FLAGS += --recreate-volume +endif +ifdef YES + COLLATION_LOAD_FLAGS += --yes +endif + +migrate-collation-load: + @if [ -z "$(SQLGZ)" ]; then \ + echo "Uzycie: make migrate-collation-load SQLGZ=/.../db-backup-...-nocollation.sql.gz [RECREATE=1]" >&2; \ + exit 1; \ + fi + @bash scripts/pg-collation-migrate-3-load.sh "$(SQLGZ)" $(COLLATION_LOAD_FLAGS) diff --git a/scripts/lib-pg-collation-migrate.sh b/scripts/lib-pg-collation-migrate.sh new file mode 100644 index 0000000..474fc9e --- /dev/null +++ b/scripts/lib-pg-collation-migrate.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# +# Wspolna biblioteka dla trzech krokow migracji "pozbadz sie kolacji libc +# pl_PL" przy przejsciu z obrazu iplweb/bpp_dbserver na stockowy postgres: +# +# pg-collation-migrate-1-dump.sh - zrzut biezacego clustra (stary obraz) +# pg-collation-migrate-2-fix.sh - usun kolacje pl_PL ze zrzutu +# pg-collation-migrate-3-load.sh - zaladuj poprawiony zrzut do psql 18 +# +# DLACZEGO to w ogole jest potrzebne: +# Stary obraz iplweb/bpp_dbserver mial wygenerowane locale libc +# pl_PL.UTF-8 (RUN localedef ...) i tworzyl kolacje +# `CREATE COLLATION public."pl_PL" (provider=libc, locale='pl_PL.UTF-8')` +# (migracja bpp 0001_collation). Oficjalny obraz `postgres` ma TYLKO +# en_US.UTF-8 + C.UTF-8 — wiec: +# * istniejacy cluster sie NIE URUCHOMI (postgresql.conf ma +# lc_messages/lc_monetary/lc_numeric/lc_time = pl_PL.utf-8, a +# pg_database datcollate/datctype = pl_PL.utf-8 — nieznane locale), +# * zrzut z `CREATE COLLATION ... libc pl_PL.UTF-8` nie wczyta sie na +# czystym obrazie. +# Kolacja byla uzywana wylacznie na stalych literalach ASCII w 5 widokach +# bpp_kronika_*_view (no-op dla sortowania), wiec mozna ja bezpiecznie +# usunac. Migracja bpp 0443_drop_pl_PL_collation robi to samo w schemacie. +# +# Strategia: logical dump (stary obraz) -> konwersja na czysty SQL z +# wycieciem kolacji -> load do swiezego clustra psql 18 zainicjowanego z +# kolacja ICU pl-PL (--locale-provider=icu --icu-locale=pl-PL). + +set -euo pipefail + +# REPO_DIR ustawia skrypt wywolujacy (dirname "$0"/..). Tu tylko walidacja. +: "${REPO_DIR:?REPO_DIR musi byc ustawione przez skrypt wywolujacy}" +REPO_ENV="$REPO_DIR/.env" + +# Obraz postgres uzywany do (a) konwersji dir-dump -> SQL w kroku 2 oraz +# (b) jako docelowy cluster w kroku 3. pg_restore z nowszego majora czyta +# starsze archiwa, wiec domyslnie bierzemy wersje docelowa. Override: +# PG_TARGET_IMAGE=postgres:18.3 bash scripts/pg-collation-migrate-2-fix.sh ... +PG_TARGET_IMAGE="${PG_TARGET_IMAGE:-postgres:${DJANGO_BPP_POSTGRESQL_VERSION:-18}}" + +# ---- Helpers ------------------------------------------------------------ + +# Czyta zmienna z pliku .env (ostatnie wystapienie wygrywa), zdejmuje +# otaczajace cudzyslowy. Identyczne zachowanie jak w scripts/restore.sh. +get_env_var() { + local raw + raw="$(grep -E "^${1}=" "$2" 2>/dev/null | tail -1 | cut -d= -f2-)" || true + if [ "${raw#\"}" != "$raw" ] && [ "${raw%\"}" != "$raw" ]; then + raw="${raw#\"}"; raw="${raw%\"}" + fi + if [ "${raw#\'}" != "$raw" ] && [ "${raw%\'}" != "$raw" ]; then + raw="${raw#\'}"; raw="${raw%\'}" + fi + printf '%s' "$raw" +} + +run() { echo "+ $*" >&2; "$@"; } + +confirm() { + local prompt="$1" answer + if [ "${NOINPUT:-0}" = 1 ]; then + echo "$prompt [auto-yes via --yes]" >&2 + return 0 + fi + read -r -p "$prompt [yes/NO]: " answer + [ "$answer" = "yes" ] +} + +# Wczytuje BPP_CONFIGS_DIR + COMPOSE_PROJECT_NAME z REPO_ENV oraz +# DJANGO_BPP_DB_* + katalog backupow z APP_ENV. Ustawia globalne zmienne: +# BPP_CONFIGS_DIR COMPOSE_PROJECT_NAME APP_ENV +# DJANGO_BPP_DB_{PASSWORD,HOST,PORT,USER,NAME} HOST_BACKUP_DIR +load_env() { + if [ ! -f "$REPO_ENV" ]; then + echo "BLAD: brak $REPO_ENV. Uruchom najpierw 'make' / 'make init-configs'." >&2 + exit 1 + fi + : "${BPP_CONFIGS_DIR:=$(get_env_var BPP_CONFIGS_DIR "$REPO_ENV")}" + : "${COMPOSE_PROJECT_NAME:=$(get_env_var COMPOSE_PROJECT_NAME "$REPO_ENV")}" + if [ -z "${BPP_CONFIGS_DIR:-}" ]; then + echo "BLAD: BPP_CONFIGS_DIR nie ustawione w $REPO_ENV." >&2; exit 1 + fi + if [ -z "${COMPOSE_PROJECT_NAME:-}" ]; then + echo "BLAD: COMPOSE_PROJECT_NAME nie ustawione w $REPO_ENV." >&2; exit 1 + fi + export COMPOSE_PROJECT_NAME + + APP_ENV="$BPP_CONFIGS_DIR/.env" + if [ ! -f "$APP_ENV" ]; then + echo "BLAD: brak $APP_ENV. Uruchom 'make init-configs'." >&2; exit 1 + fi + + DJANGO_BPP_DB_PASSWORD="$(get_env_var DJANGO_BPP_DB_PASSWORD "$APP_ENV")" + DJANGO_BPP_DB_HOST="$(get_env_var DJANGO_BPP_DB_HOST "$APP_ENV")" + DJANGO_BPP_DB_PORT="$(get_env_var DJANGO_BPP_DB_PORT "$APP_ENV")" + DJANGO_BPP_DB_USER="$(get_env_var DJANGO_BPP_DB_USER "$APP_ENV")" + DJANGO_BPP_DB_NAME="$(get_env_var DJANGO_BPP_DB_NAME "$APP_ENV")" + + HOST_BACKUP_DIR="$(get_env_var DJANGO_BPP_HOST_BACKUP_DIR "$APP_ENV")" + if [ -z "$HOST_BACKUP_DIR" ]; then + HOST_BACKUP_DIR="$(get_env_var DJANGO_BPP_BACKUP_DIR "$APP_ENV")" + fi + if [ -z "$HOST_BACKUP_DIR" ]; then + HOST_BACKUP_DIR="$(cd "$BPP_CONFIGS_DIR/.." && pwd)/backups" + fi + if [ ! -d "$HOST_BACKUP_DIR" ]; then + echo "BLAD: katalog backupow $HOST_BACKUP_DIR nie istnieje." >&2; exit 1 + fi + + # Te zmienne sa uzywane przez skrypty sourcujace te biblioteke (export + # zaspokaja tez shellcheck SC2034 — "used externally"). + export DJANGO_BPP_DB_PASSWORD DJANGO_BPP_DB_HOST DJANGO_BPP_DB_PORT \ + DJANGO_BPP_DB_USER DJANGO_BPP_DB_NAME HOST_BACKUP_DIR +} + +# Thin wrapper na `docker compose` z jawnym plikiem + projektem (jak restore.sh). +dc() { + docker compose -f "$REPO_DIR/docker-compose.yml" "$@" +} diff --git a/scripts/pg-collation-migrate-1-dump.sh b/scripts/pg-collation-migrate-1-dump.sh new file mode 100755 index 0000000..f340722 --- /dev/null +++ b/scripts/pg-collation-migrate-1-dump.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# +# KROK 1/3 migracji "pozbadz sie kolacji libc pl_PL" (przejscie z obrazu +# iplweb/bpp_dbserver na stockowy postgres). Patrz lib-pg-collation-migrate.sh. +# +# Robi logical dump BIEZACEGO clustra (ktory wciaz chodzi na STARYM obrazie +# iplweb/bpp_dbserver, bo tylko on ma locale libc pl_PL.UTF-8). Format +# katalogowy (-Fd) + tar, dokladnie jak `make db-backup`, zeby krok 2 mial +# wejscie. +# +# WAZNE: zanim zrzucisz, zatrzymaj zapisy. Domyslnie skrypt NIE zatrzymuje +# aplikacji; podaj --stop-app, zeby zatrzymal appserver + workery + beat + +# denorm-queue (jak `make restore`). Alternatywnie zrob recznie: +# docker compose stop appserver workerserver celerybeat denorm-queue +# +# Uzycie: +# bash scripts/pg-collation-migrate-1-dump.sh [--stop-app] [--yes] +# +# Wypisuje na STDOUT pelna sciezke do tarballa (do podania krokowi 2). + +set -euo pipefail +REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" +# shellcheck source=scripts/lib-pg-collation-migrate.sh +. "$REPO_DIR/scripts/lib-pg-collation-migrate.sh" + +STOP_APP=0 +NOINPUT=0 +while [ $# -gt 0 ]; do + case "$1" in + --stop-app) STOP_APP=1; shift ;; + --yes|--noinput|--non-interactive) NOINPUT=1; shift ;; + -h|--help) + sed -n '2,24p' "$0"; exit 0 ;; + *) echo "BLAD: nieznany argument: $1" >&2; exit 1 ;; + esac +done + +load_env +PARALLEL_JOBS="${PARALLEL_JOBS:-4}" +TS="$(date +%Y%m%d-%H%M%S)" +DUMP_DIRNAME="db-backup-${TS}" +DUMP_TAR="${DUMP_DIRNAME}.tar.gz" + +if ! dc ps --status=running --services 2>/dev/null | grep -qx dbserver; then + echo "BLAD: serwis 'dbserver' nie dziala. Uruchom 'make up' przed zrzutem." >&2 + exit 1 +fi + +if [ "$STOP_APP" = 1 ]; then + echo ">> Zatrzymuje aplikacje (appserver + workery + beat + denorm-queue)..." >&2 + run dc stop appserver workerserver celerybeat denorm-queue +else + echo ">> UWAGA: aplikacja NIE jest zatrzymywana (--stop-app pominiete)." >&2 + echo " Zrzut spojny tylko jesli nie ma rownoleglych zapisow do bazy." >&2 + confirm "Kontynuowac zrzut bez zatrzymania aplikacji?" || { echo "Przerwano."; exit 1; } +fi + +echo ">> pg_dump -Fd -j ${PARALLEL_JOBS} (zrodlo: stary cluster) -> /backup/${DUMP_DIRNAME}" >&2 +dc exec -T -e "PGPASSWORD=${DJANGO_BPP_DB_PASSWORD}" dbserver pg_dump \ + -Fd -j "${PARALLEL_JOBS}" \ + -h "${DJANGO_BPP_DB_HOST}" -p "${DJANGO_BPP_DB_PORT}" \ + -U "${DJANGO_BPP_DB_USER}" "${DJANGO_BPP_DB_NAME}" \ + -f "/backup/${DUMP_DIRNAME}" + +echo ">> Pakuje ${DUMP_TAR}..." >&2 +dc exec -T dbserver tar czf "/backup/${DUMP_TAR}" -C /backup "${DUMP_DIRNAME}" +dc exec -T dbserver rm -rf "/backup/${DUMP_DIRNAME}" + +echo ">> Gotowe. Tarball:" >&2 +# Jedyne, co idzie na czysty STDOUT — sciezka do tarballa (dla kroku 2). +printf '%s\n' "${HOST_BACKUP_DIR}/${DUMP_TAR}" diff --git a/scripts/pg-collation-migrate-2-fix.sh b/scripts/pg-collation-migrate-2-fix.sh new file mode 100755 index 0000000..3a9629c --- /dev/null +++ b/scripts/pg-collation-migrate-2-fix.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# +# KROK 2/3 migracji "pozbadz sie kolacji libc pl_PL". Patrz +# lib-pg-collation-migrate.sh. +# +# Bierze tarball ze zrzutem katalogowym (-Fd) z kroku 1, konwertuje go na +# czysty SQL i WYCINA kolacje pl_PL: +# * usuwa CREATE/ALTER/COMMENT ... COLLATION ... "pl_PL" +# * usuwa klauzule COLLATE [public.]"pl_PL" (z 5 widokow bpp_kronika_*) +# Wynik: -nocollation.sql.gz w katalogu backupow — wejscie kroku 3. +# +# DLACZEGO przez czysty SQL, a nie pg_restore -L: format katalogowy trzyma +# DDL w binarnym toc.dat. `pg_restore -L` umie pominac OBIEKT kolacji, ale +# NIE umie wyciac klauzul COLLATE wstrzyknietych w definicje widokow. Wiec +# konwertujemy do SQL (pg_restore -f -), sed-ujemy, ladujemy psql-em (krok 3). +# Koszt: load jest jednowatkowy (psql -f) zamiast rownoleglego pg_restore -j. +# +# Uzycie: +# bash scripts/pg-collation-migrate-2-fix.sh +# +# pg_restore bierzemy z obrazu $PG_TARGET_IMAGE (domyslnie wersja docelowa, +# nowszy pg_restore czyta starsze archiwa). + +set -euo pipefail +REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" +# shellcheck source=scripts/lib-pg-collation-migrate.sh +. "$REPO_DIR/scripts/lib-pg-collation-migrate.sh" + +if [ $# -lt 1 ] || [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then + sed -n '2,20p' "$0"; exit 0 +fi + +SRC_TAR="$1" +[ -f "$SRC_TAR" ] || { echo "BLAD: nie ma pliku: $SRC_TAR" >&2; exit 1; } +SRC_TAR="$(cd "$(dirname "$SRC_TAR")" && pwd)/$(basename "$SRC_TAR")" + +BASE="$(basename "$SRC_TAR")"; BASE="${BASE%.tar.gz}"; BASE="${BASE%.tgz}" +OUT_DIR="$(dirname "$SRC_TAR")" +OUT_SQL_GZ="${OUT_DIR}/${BASE}-nocollation.sql.gz" + +TMP="$(mktemp -d)" +cleanup() { rm -rf "$TMP"; } +trap cleanup EXIT + +echo ">> Rozpakowuje $BASE ..." >&2 +tar xzf "$SRC_TAR" -C "$TMP" +DUMP_DIR="$(find "$TMP" -maxdepth 1 -mindepth 1 -type d | head -1)" +if [ -z "$DUMP_DIR" ] || [ ! -f "$DUMP_DIR/toc.dat" ]; then + echo "BLAD: w tarballu nie ma katalogowego zrzutu pg_dump (-Fd, brak toc.dat)." >&2 + exit 1 +fi + +echo ">> pg_restore -f - (obraz: $PG_TARGET_IMAGE) | sed (wycinam kolacje pl_PL) | gzip" >&2 +# pg_restore -f - nie potrzebuje uruchomionego serwera: zamienia archiwum na +# SQL na stdout. Sed na hoscie wycina kolacje, gzip zapisuje wynik. +set -o pipefail +docker run --rm -v "$TMP:/dump:ro" "$PG_TARGET_IMAGE" \ + pg_restore -f - "/dump/$(basename "$DUMP_DIR")" \ + | sed -E \ + -e '/^CREATE COLLATION (public\.)?"?pl_PL"?/d' \ + -e '/^ALTER COLLATION (public\.)?"?pl_PL"?/d' \ + -e '/^COMMENT ON COLLATION (public\.)?"?pl_PL"?/d' \ + -e 's/ COLLATE (public\.)?"pl_PL"//g' \ + | gzip > "$OUT_SQL_GZ" + +echo ">> Weryfikacja: zadnych pozostalosci kolacji pl_PL w wyniku..." >&2 +if zgrep -nE 'COLLATION[[:space:]]+("?public"?\.)?"?pl_PL|COLLATE[[:space:]]+(public\.)?"pl_PL"' \ + "$OUT_SQL_GZ" >/dev/null 2>&1; then + echo "BLAD: w wyniku nadal sa odwolania do kolacji pl_PL:" >&2 + zgrep -nE 'COLLATION[[:space:]]+("?public"?\.)?"?pl_PL|COLLATE[[:space:]]+(public\.)?"pl_PL"' \ + "$OUT_SQL_GZ" | head >&2 + rm -f "$OUT_SQL_GZ" + exit 1 +fi + +# Sanity: zrzut sprzed migracji bpp 0442 ma jeszcze plpython3u, ktorego stock +# postgres nie ma -> load padnie. Ostrzegamy (nie blokujemy). +if zgrep -qiE 'plpython3u|LANGUAGE plpython' "$OUT_SQL_GZ"; then + echo "!! OSTRZEZENIE: zrzut zawiera plpython3u (sprzed migracji bpp 0442)." >&2 + echo " Stock postgres go nie ma -> krok 3 padnie. Zrob NOWY zrzut po" >&2 + echo " wdrozeniu wersji aplikacji z migracja 0442 (DROP EXTENSION plpython3u)." >&2 +fi + +echo ">> Gotowe. Poprawiony zrzut:" >&2 +printf '%s\n' "$OUT_SQL_GZ" diff --git a/scripts/pg-collation-migrate-3-load.sh b/scripts/pg-collation-migrate-3-load.sh new file mode 100755 index 0000000..9ba0477 --- /dev/null +++ b/scripts/pg-collation-migrate-3-load.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# +# KROK 3/3 migracji "pozbadz sie kolacji libc pl_PL". Patrz +# lib-pg-collation-migrate.sh. +# +# Laduje poprawiony zrzut (<...>-nocollation.sql.gz z kroku 2) do SWIEZEGO +# clustra na STOCKOWYM obrazie postgres (np. 18), zainicjowanego z kolacja +# ICU pl-PL (POSTGRES_INITDB_ARGS=--locale-provider=icu --icu-locale=pl-PL, +# ustawiane w docker-compose.database.yml). +# +# Zaklada, ze serwis 'dbserver' chodzi juz na NOWYM obrazie (stock postgres) +# na PUSTYM volume. Jesli nie — uzyj --recreate-volume, zeby skrypt: +# 1) zatrzymal appserver + workery + dbserver, +# 2) USUNAL volume ${COMPOSE_PROJECT_NAME}_postgresql_data (DESTRUKCYJNE!), +# 3) wstal dbserver na nowo (initdb wg obrazu z docker-compose.database.yml). +# Najpierw ustaw w $BPP_CONFIGS_DIR/.env DJANGO_BPP_POSTGRESQL_VERSION=18.x +# i upewnij sie, ze docker-compose.database.yml uzywa stockowego postgres. +# +# Uzycie: +# bash scripts/pg-collation-migrate-3-load.sh \ +# [--recreate-volume] [--yes] + +set -euo pipefail +REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" +# shellcheck source=scripts/lib-pg-collation-migrate.sh +. "$REPO_DIR/scripts/lib-pg-collation-migrate.sh" + +SQL_GZ="" +RECREATE=0 +NOINPUT=0 +while [ $# -gt 0 ]; do + case "$1" in + --recreate-volume) RECREATE=1; shift ;; + --yes|--noinput|--non-interactive) NOINPUT=1; shift ;; + -h|--help) sed -n '2,22p' "$0"; exit 0 ;; + -*) echo "BLAD: nieznany argument: $1" >&2; exit 1 ;; + *) SQL_GZ="$1"; shift ;; + esac +done +if [ -z "$SQL_GZ" ] || [ ! -f "$SQL_GZ" ]; then + echo "BLAD: podaj istniejacy plik *-nocollation.sql.gz" >&2 + exit 1 +fi +SQL_GZ="$(cd "$(dirname "$SQL_GZ")" && pwd)/$(basename "$SQL_GZ")" + +load_env + +if [ "$RECREATE" = 1 ]; then + VOL="${COMPOSE_PROJECT_NAME}_postgresql_data" + echo "!! --recreate-volume: USUNE volume '$VOL' i wszystkie dane biezacego clustra." >&2 + confirm "Na pewno skasowac volume '$VOL' i postawic SWIEZY cluster?" \ + || { echo "Przerwano — nic nie ruszone." >&2; exit 1; } + run dc stop appserver workerserver celerybeat denorm-queue dbserver || true + run dc rm -f dbserver || true + run docker volume rm "$VOL" + echo ">> Stawiam swiezy dbserver (initdb wg obrazu z compose)..." >&2 + run dc up -d --wait dbserver +fi + +if ! dc ps --status=running --services 2>/dev/null | grep -qx dbserver; then + echo "BLAD: serwis 'dbserver' nie dziala. Uruchom 'make up' lub uzyj --recreate-volume." >&2 + exit 1 +fi + +# Bezpiecznik: nie laduj do STAREGO obrazu (bylby to stary cluster, nie psql 18). +IMG="$(dc ps --format '{{.Image}}' dbserver 2>/dev/null | head -1 || true)" +echo ">> dbserver image: ${IMG:-?}" >&2 +if printf '%s' "$IMG" | grep -qi 'bpp_dbserver'; then + echo "BLAD: dbserver wciaz uzywa obrazu iplweb/bpp_dbserver. Przelacz na stock" >&2 + echo " postgres (docker-compose.database.yml + DJANGO_BPP_POSTGRESQL_VERSION)" >&2 + echo " i uzyj --recreate-volume, zanim zaladujesz zrzut." >&2 + exit 1 +fi + +PSQL=(dc exec -T -e "PGPASSWORD=${DJANGO_BPP_DB_PASSWORD}" dbserver psql \ + -h "${DJANGO_BPP_DB_HOST}" -p "${DJANGO_BPP_DB_PORT}" -U "${DJANGO_BPP_DB_USER}") + +echo ">> dropdb --force + createdb ${DJANGO_BPP_DB_NAME} (dziedziczy kolacje ICU pl-PL z clustra)" >&2 +confirm "Skasowac i odtworzyc baze '${DJANGO_BPP_DB_NAME}' na ${IMG}?" \ + || { echo "Przerwano." >&2; exit 1; } +dc exec -T -e "PGPASSWORD=${DJANGO_BPP_DB_PASSWORD}" dbserver \ + dropdb --force --if-exists -h "${DJANGO_BPP_DB_HOST}" -p "${DJANGO_BPP_DB_PORT}" \ + -U "${DJANGO_BPP_DB_USER}" "${DJANGO_BPP_DB_NAME}" +dc exec -T -e "PGPASSWORD=${DJANGO_BPP_DB_PASSWORD}" dbserver \ + createdb -h "${DJANGO_BPP_DB_HOST}" -p "${DJANGO_BPP_DB_PORT}" \ + -U "${DJANGO_BPP_DB_USER}" "${DJANGO_BPP_DB_NAME}" + +echo ">> Sprawdzam kolacje docelowej bazy (oczekiwane: ICU / 'i')..." >&2 +PROV="$("${PSQL[@]}" -tAc \ + "SELECT datlocprovider FROM pg_database WHERE datname='${DJANGO_BPP_DB_NAME}';" 2>/dev/null | tr -d '[:space:]')" +echo " datlocprovider=${PROV:-?}" >&2 +if [ "${PROV:-}" != "i" ]; then + echo "!! OSTRZEZENIE: docelowa baza nie jest ICU. Czy cluster zainicjowano z" >&2 + echo " POSTGRES_INITDB_ARGS=--locale-provider=icu --icu-locale=pl-PL ?" >&2 +fi + +echo ">> Laduje ${SQL_GZ} (psql, ON_ERROR_STOP=1)..." >&2 +gunzip -c "$SQL_GZ" | "${PSQL[@]}" -v ON_ERROR_STOP=1 -d "${DJANGO_BPP_DB_NAME}" + +echo ">> Weryfikacja po loadzie:" >&2 +"${PSQL[@]}" -d "${DJANGO_BPP_DB_NAME}" -tAc \ + "SELECT 'kronika views: '||count(*) FROM information_schema.views WHERE table_name LIKE 'bpp_kronika%';" >&2 +"${PSQL[@]}" -d "${DJANGO_BPP_DB_NAME}" -tAc \ + "SELECT 'collations pl_PL pozostale: '||count(*) FROM pg_collation WHERE collname='pl_PL';" >&2 +echo ">> Gotowe. Baza '${DJANGO_BPP_DB_NAME}' zaladowana na ${IMG}." >&2 +echo " Teraz: zmigruj aplikacje ('make migrate') i wstan stack ('make up')." >&2 From c267ae86ab69f72ccf175c1f606b96c017237744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sat, 13 Jun 2026 20:58:43 +0200 Subject: [PATCH 03/15] =?UTF-8?q?refactor(collation-migrate):=20plain=20SQ?= =?UTF-8?q?L=20end-to-end,=20drop=20-Fd=E2=86=92pg=5Frestore=20detour?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Krok 1 zrzucał format katalogowy (-Fd) + tar, co zmuszało krok 2 do konwersji `pg_restore -f -` (binarny toc.dat) → SQL, tylko po to żeby go zesed-ować. Format binarny nic tu nie dawał: kolację trzeba wyciąć z TEKSTU definicji widoków (COLLATE "pl_PL"), a load i tak idzie psql-em (jednowątkowo), więc równoległość pg_restore -j była martwa. Teraz plain SQL przez cały pipeline: - krok 1: `pg_dump -Fp | gzip` → db-backup-.sql.gz (in-container dump, host-side gzip; trap czyści .partial gdy pg_dump padnie; gzip -t) - krok 2: `gunzip | sed | gzip` — bez docker run, bez pg_restore, bez obrazu postgres, bez tar; guard odrzuca stary .tar.gz; gzip -t na wejściu - krok 3: bez zmian Usunięto PG_TARGET_IMAGE z lib (był tylko do konwersji w kroku 2). Makefile: migrate-collation-fix TARBALL= → DUMPGZ=. Runbook zaktualizowany. Zweryfikowane na realnym prod-dumpie: 7 ref. kolacji → 0, ostrzeżenie plpython działa, guard .tar.gz odrzuca stary format. shellcheck Passed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../migracja-collation-stock-pg.md | 24 ++++---- mk/database.mk | 8 +-- scripts/lib-pg-collation-migrate.sh | 17 +++--- scripts/pg-collation-migrate-1-dump.sh | 51 ++++++++++------ scripts/pg-collation-migrate-2-fix.sh | 59 ++++++++----------- 5 files changed, 85 insertions(+), 74 deletions(-) diff --git a/docs/eksploatacja/migracja-collation-stock-pg.md b/docs/eksploatacja/migracja-collation-stock-pg.md index ddfa2b0..bcc2840 100644 --- a/docs/eksploatacja/migracja-collation-stock-pg.md +++ b/docs/eksploatacja/migracja-collation-stock-pg.md @@ -58,18 +58,19 @@ make migrate-collation-dump # lub: bash scripts/pg-collation-migrate-1-dump.sh --stop-app ``` -Wypisze ścieżkę do tarballa, np. -`/…/backups/db-backup-20260613-190000.tar.gz` (format katalogowy `pg_dump -Fd`, -tak jak `make db-backup`). +Wypisze ścieżkę do zrzutu, np. +`/…/backups/db-backup-20260613-190000.sql.gz` (plain SQL `pg_dump -Fp` +spakowany gzipem — sam w sobie ładowalny backup). ### 2. Usuń kolację `pl_PL` ze zrzutu ```bash -make migrate-collation-fix TARBALL=/…/backups/db-backup-20260613-190000.tar.gz -# lub: bash scripts/pg-collation-migrate-2-fix.sh +make migrate-collation-fix DUMPGZ=/…/backups/db-backup-20260613-190000.sql.gz +# lub: bash scripts/pg-collation-migrate-2-fix.sh <…sql.gz> ``` -Konwertuje zrzut katalogowy na czysty SQL (`pg_restore -f -`), wycina +Czysta transformacja tekstu na hoscie — `gunzip | sed | gzip`, bez +`pg_restore`, bez obrazu postgres, bez tar. Wycina `CREATE/ALTER/COMMENT … COLLATION … "pl_PL"` oraz klauzule `COLLATE "pl_PL"`, i zapisuje `…-nocollation.sql.gz`. Weryfikuje brak pozostałości i ostrzega o plpython. Jest **idempotentny** — jeśli zrzut był już zrobiony po migracji @@ -113,10 +114,13 @@ make dbshell-psql # potem: ## Uwagi i ograniczenia -- **Load jest jednowątkowy** (`psql -f`), bo wycinamy kolację na poziomie - czystego SQL (format katalogowy trzyma DDL w binarnym `toc.dat`, więc - `pg_restore -L` nie wytnie klauzul `COLLATE` z definicji widoków). Dla bardzo - dużych baz to wolniejsze niż `pg_restore -Fd -j`, ale to operacja jednorazowa. +- **Cały pipeline jest plain SQL** (dump `-Fp` → `sed` → `psql -f`), bo kolację + trzeba wyciąć z **tekstu** definicji widoków (`COLLATE "pl_PL"`), czego format + katalogowy/custom (binarny `toc.dat`) nie pozwala zrobić bez konwersji + `pg_restore -f -`. Load i tak jest jednowątkowy (`psql`), więc równoległość + `pg_restore -Fd -j` nic by tu nie dała — dlatego żaden binarny pośrednik nie + jest potrzebny. Dla bardzo dużych baz dump/load jest sekwencyjny, ale to + operacja jednorazowa. - **`make backup` wciąż działa ze starym obrazem** — obrazy `iplweb/bpp_dbserver` są nadal na Docker Hubie, więc dump da się zrobić nawet po decyzji o migracji. - Skrypt kroku 3 **odmówi** załadowania do kontenera nadal działającego na diff --git a/mk/database.mk b/mk/database.mk index b337bb1..231feb9 100644 --- a/mk/database.mk +++ b/mk/database.mk @@ -180,7 +180,7 @@ test-upgrade-postgres: # na scripts/pg-collation-migrate-{1-dump,2-fix,3-load}.sh). Pelny opis: # docs/eksploatacja/migracja-collation-stock-pg.md. # make migrate-collation-dump [STOP_APP=1] [YES=1] -# make migrate-collation-fix TARBALL=/.../db-backup-*.tar.gz +# make migrate-collation-fix DUMPGZ=/.../db-backup-*.sql.gz # make migrate-collation-load SQLGZ=/.../*-nocollation.sql.gz [RECREATE=1] [YES=1] COLLATION_DUMP_FLAGS := ifdef STOP_APP @@ -194,11 +194,11 @@ migrate-collation-dump: @bash scripts/pg-collation-migrate-1-dump.sh $(COLLATION_DUMP_FLAGS) migrate-collation-fix: - @if [ -z "$(TARBALL)" ]; then \ - echo "Uzycie: make migrate-collation-fix TARBALL=/.../db-backup-YYYYMMDD-HHMMSS.tar.gz" >&2; \ + @if [ -z "$(DUMPGZ)" ]; then \ + echo "Uzycie: make migrate-collation-fix DUMPGZ=/.../db-backup-YYYYMMDD-HHMMSS.sql.gz" >&2; \ exit 1; \ fi - @bash scripts/pg-collation-migrate-2-fix.sh "$(TARBALL)" + @bash scripts/pg-collation-migrate-2-fix.sh "$(DUMPGZ)" COLLATION_LOAD_FLAGS := ifdef RECREATE diff --git a/scripts/lib-pg-collation-migrate.sh b/scripts/lib-pg-collation-migrate.sh index 474fc9e..f4e57e1 100644 --- a/scripts/lib-pg-collation-migrate.sh +++ b/scripts/lib-pg-collation-migrate.sh @@ -22,9 +22,14 @@ # bpp_kronika_*_view (no-op dla sortowania), wiec mozna ja bezpiecznie # usunac. Migracja bpp 0443_drop_pl_PL_collation robi to samo w schemacie. # -# Strategia: logical dump (stary obraz) -> konwersja na czysty SQL z -# wycieciem kolacji -> load do swiezego clustra psql 18 zainicjowanego z -# kolacja ICU pl-PL (--locale-provider=icu --icu-locale=pl-PL). +# Strategia: zrzut do PLAIN SQL (pg_dump -Fp) -> tekstowe wyciecie kolacji +# (sed) -> load do swiezego clustra psql 18 zainicjowanego z kolacja ICU +# pl-PL (--locale-provider=icu --icu-locale=pl-PL). Plain SQL przez caly +# czas — bo i tak musimy EDYTOWAC TEKST definicji widokow (usunac klauzule +# COLLATE), czego format katalogowy/custom (binarny toc.dat) nie pozwala +# zrobic bez konwersji `pg_restore -f -`. A skoro load i tak idzie psql-em +# (jednowatkowo), rownoleglosc pg_restore -j nic by tu nie dala. Wiec zaden +# binarny posrednik nie jest potrzebny. set -euo pipefail @@ -32,12 +37,6 @@ set -euo pipefail : "${REPO_DIR:?REPO_DIR musi byc ustawione przez skrypt wywolujacy}" REPO_ENV="$REPO_DIR/.env" -# Obraz postgres uzywany do (a) konwersji dir-dump -> SQL w kroku 2 oraz -# (b) jako docelowy cluster w kroku 3. pg_restore z nowszego majora czyta -# starsze archiwa, wiec domyslnie bierzemy wersje docelowa. Override: -# PG_TARGET_IMAGE=postgres:18.3 bash scripts/pg-collation-migrate-2-fix.sh ... -PG_TARGET_IMAGE="${PG_TARGET_IMAGE:-postgres:${DJANGO_BPP_POSTGRESQL_VERSION:-18}}" - # ---- Helpers ------------------------------------------------------------ # Czyta zmienna z pliku .env (ostatnie wystapienie wygrywa), zdejmuje diff --git a/scripts/pg-collation-migrate-1-dump.sh b/scripts/pg-collation-migrate-1-dump.sh index f340722..46b5329 100755 --- a/scripts/pg-collation-migrate-1-dump.sh +++ b/scripts/pg-collation-migrate-1-dump.sh @@ -4,9 +4,16 @@ # iplweb/bpp_dbserver na stockowy postgres). Patrz lib-pg-collation-migrate.sh. # # Robi logical dump BIEZACEGO clustra (ktory wciaz chodzi na STARYM obrazie -# iplweb/bpp_dbserver, bo tylko on ma locale libc pl_PL.UTF-8). Format -# katalogowy (-Fd) + tar, dokladnie jak `make db-backup`, zeby krok 2 mial -# wejscie. +# iplweb/bpp_dbserver, bo tylko on ma locale libc pl_PL.UTF-8) do PLAIN SQL +# (pg_dump -Fp), spakowanego gzipem -> db-backup-.sql.gz. +# +# DLACZEGO plain SQL, a nie format katalogowy (-Fd) jak `make db-backup`: +# krok 2 musi EDYTOWAC TEKST (sed) definicji widokow, zeby wyciac klauzule +# COLLATE "pl_PL". Format binarny (-Fd/-Fc) trzeba by najpierw skonwertowac +# `pg_restore -f -` (dodatkowy obraz postgres na hoscie + tar/untar) — a +# skoro load i tak idzie psql-em (jednowatkowo), rownoleglosc pg_restore -j +# nic nie daje. Plain SQL = zaden binarny posrednik. Wynikowy .sql.gz jest +# tez normalnym, ladowalnym backupem. # # WAZNE: zanim zrzucisz, zatrzymaj zapisy. Domyslnie skrypt NIE zatrzymuje # aplikacji; podaj --stop-app, zeby zatrzymal appserver + workery + beat + @@ -16,7 +23,7 @@ # Uzycie: # bash scripts/pg-collation-migrate-1-dump.sh [--stop-app] [--yes] # -# Wypisuje na STDOUT pelna sciezke do tarballa (do podania krokowi 2). +# Wypisuje na STDOUT pelna sciezke do .sql.gz (do podania krokowi 2). set -euo pipefail REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" @@ -30,16 +37,21 @@ while [ $# -gt 0 ]; do --stop-app) STOP_APP=1; shift ;; --yes|--noinput|--non-interactive) NOINPUT=1; shift ;; -h|--help) - sed -n '2,24p' "$0"; exit 0 ;; + sed -n '2,30p' "$0"; exit 0 ;; *) echo "BLAD: nieznany argument: $1" >&2; exit 1 ;; esac done load_env -PARALLEL_JOBS="${PARALLEL_JOBS:-4}" TS="$(date +%Y%m%d-%H%M%S)" -DUMP_DIRNAME="db-backup-${TS}" -DUMP_TAR="${DUMP_DIRNAME}.tar.gz" +DUMP_SQL_GZ="db-backup-${TS}.sql.gz" +OUT_PATH="${HOST_BACKUP_DIR}/${DUMP_SQL_GZ}" +PARTIAL="${OUT_PATH}.partial" + +# Niedokonczony zrzut (pg_dump padl w polowie potoku) nie moze udawac +# gotowego pliku — krok 2 by go pozniej sed-owal jako kompletny. +cleanup_partial() { rm -f "$PARTIAL"; } +trap cleanup_partial EXIT if ! dc ps --status=running --services 2>/dev/null | grep -qx dbserver; then echo "BLAD: serwis 'dbserver' nie dziala. Uruchom 'make up' przed zrzutem." >&2 @@ -55,17 +67,22 @@ else confirm "Kontynuowac zrzut bez zatrzymania aplikacji?" || { echo "Przerwano."; exit 1; } fi -echo ">> pg_dump -Fd -j ${PARALLEL_JOBS} (zrodlo: stary cluster) -> /backup/${DUMP_DIRNAME}" >&2 +# pg_dump -Fp pisze czysty SQL na stdout (in-container), gzip pakuje na +# hoscie. Brak -j (plain SQL nie wspiera rownoleglosci) i tak nie jest +# strata: load w kroku 3 to jednowatkowy psql. -T wylacza pseudo-TTY, wiec +# strumien nie jest psuty translacja CR/LF. pipefail (z set -o) wylapie +# blad pg_dump mimo gzipa po prawej stronie potoku. +echo ">> pg_dump -Fp (zrodlo: stary cluster) | gzip -> ${OUT_PATH}" >&2 dc exec -T -e "PGPASSWORD=${DJANGO_BPP_DB_PASSWORD}" dbserver pg_dump \ - -Fd -j "${PARALLEL_JOBS}" \ + -Fp \ -h "${DJANGO_BPP_DB_HOST}" -p "${DJANGO_BPP_DB_PORT}" \ -U "${DJANGO_BPP_DB_USER}" "${DJANGO_BPP_DB_NAME}" \ - -f "/backup/${DUMP_DIRNAME}" + | gzip > "$PARTIAL" -echo ">> Pakuje ${DUMP_TAR}..." >&2 -dc exec -T dbserver tar czf "/backup/${DUMP_TAR}" -C /backup "${DUMP_DIRNAME}" -dc exec -T dbserver rm -rf "/backup/${DUMP_DIRNAME}" +echo ">> Sprawdzam integralnosc gzipa..." >&2 +gzip -t "$PARTIAL" +mv "$PARTIAL" "$OUT_PATH" -echo ">> Gotowe. Tarball:" >&2 -# Jedyne, co idzie na czysty STDOUT — sciezka do tarballa (dla kroku 2). -printf '%s\n' "${HOST_BACKUP_DIR}/${DUMP_TAR}" +echo ">> Gotowe. Zrzut plain SQL:" >&2 +# Jedyne, co idzie na czysty STDOUT — sciezka do .sql.gz (dla kroku 2). +printf '%s\n' "$OUT_PATH" diff --git a/scripts/pg-collation-migrate-2-fix.sh b/scripts/pg-collation-migrate-2-fix.sh index 3a9629c..e427299 100755 --- a/scripts/pg-collation-migrate-2-fix.sh +++ b/scripts/pg-collation-migrate-2-fix.sh @@ -3,23 +3,19 @@ # KROK 2/3 migracji "pozbadz sie kolacji libc pl_PL". Patrz # lib-pg-collation-migrate.sh. # -# Bierze tarball ze zrzutem katalogowym (-Fd) z kroku 1, konwertuje go na -# czysty SQL i WYCINA kolacje pl_PL: +# Bierze zrzut plain SQL (db-backup-.sql.gz z kroku 1) i WYCINA z niego +# kolacje pl_PL czystym sed-em: # * usuwa CREATE/ALTER/COMMENT ... COLLATION ... "pl_PL" # * usuwa klauzule COLLATE [public.]"pl_PL" (z 5 widokow bpp_kronika_*) # Wynik: -nocollation.sql.gz w katalogu backupow — wejscie kroku 3. # -# DLACZEGO przez czysty SQL, a nie pg_restore -L: format katalogowy trzyma -# DDL w binarnym toc.dat. `pg_restore -L` umie pominac OBIEKT kolacji, ale -# NIE umie wyciac klauzul COLLATE wstrzyknietych w definicje widokow. Wiec -# konwertujemy do SQL (pg_restore -f -), sed-ujemy, ladujemy psql-em (krok 3). -# Koszt: load jest jednowatkowy (psql -f) zamiast rownoleglego pg_restore -j. +# To czysta transformacja tekstu na hoscie: gunzip | sed | gzip. Zaden +# pg_restore, zaden obraz postgres, zaden tar — bo krok 1 daje juz plain +# SQL (patrz lib: kolacja siedzi w TEKSCIE definicji widokow, a tego nie +# da sie wyciac z binarnego -Fd inaczej niz konwertujac go wpierw na SQL). # # Uzycie: -# bash scripts/pg-collation-migrate-2-fix.sh -# -# pg_restore bierzemy z obrazu $PG_TARGET_IMAGE (domyslnie wersja docelowa, -# nowszy pg_restore czyta starsze archiwa). +# bash scripts/pg-collation-migrate-2-fix.sh set -euo pipefail REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" @@ -27,35 +23,30 @@ REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" . "$REPO_DIR/scripts/lib-pg-collation-migrate.sh" if [ $# -lt 1 ] || [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then - sed -n '2,20p' "$0"; exit 0 + sed -n '2,21p' "$0"; exit 0 fi -SRC_TAR="$1" -[ -f "$SRC_TAR" ] || { echo "BLAD: nie ma pliku: $SRC_TAR" >&2; exit 1; } -SRC_TAR="$(cd "$(dirname "$SRC_TAR")" && pwd)/$(basename "$SRC_TAR")" +SRC_GZ="$1" +[ -f "$SRC_GZ" ] || { echo "BLAD: nie ma pliku: $SRC_GZ" >&2; exit 1; } +SRC_GZ="$(cd "$(dirname "$SRC_GZ")" && pwd)/$(basename "$SRC_GZ")" -BASE="$(basename "$SRC_TAR")"; BASE="${BASE%.tar.gz}"; BASE="${BASE%.tgz}" -OUT_DIR="$(dirname "$SRC_TAR")" -OUT_SQL_GZ="${OUT_DIR}/${BASE}-nocollation.sql.gz" +# Lapiemy stary, binarny format z poprzedniej wersji skryptu (-Fd tarball). +case "$SRC_GZ" in + *.tar.gz|*.tgz) + echo "BLAD: to wyglada na tarball (-Fd). Krok 1 produkuje teraz plain" >&2 + echo " SQL (db-backup-.sql.gz) — podaj plik .sql.gz." >&2 + exit 1 ;; +esac -TMP="$(mktemp -d)" -cleanup() { rm -rf "$TMP"; } -trap cleanup EXIT +BASE="$(basename "$SRC_GZ")"; BASE="${BASE%.sql.gz}"; BASE="${BASE%.gz}" +OUT_DIR="$(dirname "$SRC_GZ")" +OUT_SQL_GZ="${OUT_DIR}/${BASE}-nocollation.sql.gz" -echo ">> Rozpakowuje $BASE ..." >&2 -tar xzf "$SRC_TAR" -C "$TMP" -DUMP_DIR="$(find "$TMP" -maxdepth 1 -mindepth 1 -type d | head -1)" -if [ -z "$DUMP_DIR" ] || [ ! -f "$DUMP_DIR/toc.dat" ]; then - echo "BLAD: w tarballu nie ma katalogowego zrzutu pg_dump (-Fd, brak toc.dat)." >&2 - exit 1 -fi +echo ">> Sprawdzam integralnosc wejsciowego gzipa..." >&2 +gzip -t "$SRC_GZ" -echo ">> pg_restore -f - (obraz: $PG_TARGET_IMAGE) | sed (wycinam kolacje pl_PL) | gzip" >&2 -# pg_restore -f - nie potrzebuje uruchomionego serwera: zamienia archiwum na -# SQL na stdout. Sed na hoscie wycina kolacje, gzip zapisuje wynik. -set -o pipefail -docker run --rm -v "$TMP:/dump:ro" "$PG_TARGET_IMAGE" \ - pg_restore -f - "/dump/$(basename "$DUMP_DIR")" \ +echo ">> gunzip | sed (wycinam kolacje pl_PL) | gzip -> ${OUT_SQL_GZ##*/}" >&2 +gunzip -c "$SRC_GZ" \ | sed -E \ -e '/^CREATE COLLATION (public\.)?"?pl_PL"?/d' \ -e '/^ALTER COLLATION (public\.)?"?pl_PL"?/d' \ From e529df514058666e160f2fdd71c6e77d8a274bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sat, 13 Jun 2026 21:41:05 +0200 Subject: [PATCH 04/15] fix(collation-migrate): match pl_pl any-case; drop gzip, plain .sql end-to-end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug z produkcji: load padał na "could not create locale pl_PL.utf8", bo realna kolacja nazywa sie `public.pl_pl` (male litery, bez cudzyslowu, locale='pl_PL.utf8'), a sed wycinal tylko case-sensitive `pl_PL`. To samo tlumaczy czemu migracja bpp 0443 (DROP COLLATION ... "pl_PL") jej nie ruszyla. Teraz wzorzec uzywa klas znakowych [pP][lL]_[pP][lL] + opcjonalny public./cudzyslow -> lapie KAZDY wariant (pl_pl, pl_PL, "pl_PL", public.*). Dodatkowo (prosba usera): caly pipeline bez gzipa, plain .sql end-to-end: - krok 1: pg_dump -Fp -> db-backup-.sql (bez gzipa; kompletnosc przez marker 'PostgreSQL database dump complete' zamiast gzip -t; pv zostaje) - krok 2: sed in->out (bez gunzip/gzip); weryfikacja plain grep; usuwa tez naglowkowe komentarze '-- Name: pl_PL...; Type: COLLATION' - krok 3: psql < file (bez gunzip), opcjonalny pasek pv; weryfikacja kolacji liczona case-insensitive w schemacie public - Makefile: DUMPGZ->DUMPSQL, SQLGZ->SQL; guardy odrzucaja .gz/.tar.gz SC2034 (NOINPUT uzywane w confirm() z lib) wyciszone tez w kroku 3 — przy commicie lib nie jest stage'owana, wiec shellcheck nie sledzi source. Zweryfikowane na fixturze z realnym ksztaltem dumpu: 6 ref. kolacji (pl_pl/"pl_PL"/COLLATE oba) -> 0, string '0443_drop_pl_PL_collation' w django_migrations zachowany. shellcheck Passed (standalone, per plik). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../migracja-collation-stock-pg.md | 43 ++++++---- mk/database.mk | 16 ++-- scripts/pg-collation-migrate-1-dump.sh | 66 +++++++++++---- scripts/pg-collation-migrate-2-fix.sh | 83 ++++++++++--------- scripts/pg-collation-migrate-3-load.sh | 43 +++++++--- 5 files changed, 160 insertions(+), 91 deletions(-) diff --git a/docs/eksploatacja/migracja-collation-stock-pg.md b/docs/eksploatacja/migracja-collation-stock-pg.md index bcc2840..6866c5d 100644 --- a/docs/eksploatacja/migracja-collation-stock-pg.md +++ b/docs/eksploatacja/migracja-collation-stock-pg.md @@ -59,21 +59,24 @@ make migrate-collation-dump ``` Wypisze ścieżkę do zrzutu, np. -`/…/backups/db-backup-20260613-190000.sql.gz` (plain SQL `pg_dump -Fp` -spakowany gzipem — sam w sobie ładowalny backup). +`/…/backups/db-backup-20260613-190000.sql` (plain SQL `pg_dump -Fp`, **bez +gzipa** — sam w sobie ładowalny backup). Jeśli na hoście jest `pv`, leci pasek +postępu. ### 2. Usuń kolację `pl_PL` ze zrzutu ```bash -make migrate-collation-fix DUMPGZ=/…/backups/db-backup-20260613-190000.sql.gz -# lub: bash scripts/pg-collation-migrate-2-fix.sh <…sql.gz> +make migrate-collation-fix DUMPSQL=/…/backups/db-backup-20260613-190000.sql +# lub: bash scripts/pg-collation-migrate-2-fix.sh <…sql> ``` -Czysta transformacja tekstu na hoscie — `gunzip | sed | gzip`, bez +Czysta transformacja tekstu na hoście — `sed` in → out, bez gzipa, bez `pg_restore`, bez obrazu postgres, bez tar. Wycina -`CREATE/ALTER/COMMENT … COLLATION … "pl_PL"` oraz klauzule `COLLATE "pl_PL"`, -i zapisuje `…-nocollation.sql.gz`. Weryfikuje brak pozostałości i ostrzega o -plpython. Jest **idempotentny** — jeśli zrzut był już zrobiony po migracji +`CREATE/ALTER/COMMENT … COLLATION … pl_PL` oraz klauzule `COLLATE pl_PL`, +i zapisuje `…-nocollation.sql`. **Nazwa kolacji jest case-insensitive i może +być w cudzysłowie lub bez** — realne bazy mają `public.pl_pl` (małe litery, +`locale='pl_PL.utf8'`), nie `"pl_PL"`. Weryfikuje brak pozostałości i ostrzega +o plpython. Jest **idempotentny** — jeśli zrzut był już zrobiony po migracji 0443 (bez kolacji), po prostu nic nie znajdzie. ### 3. Załaduj do świeżego klastra psql 18 @@ -82,15 +85,15 @@ Najpierw postaw **świeży** klaster na nowym obrazie. Albo przez istniejący `make upgrade-postgres`, albo pozwól zrobić to skryptowi (`--recreate-volume`): ```bash -make migrate-collation-load SQLGZ=/…/backups/db-backup-20260613-190000-nocollation.sql.gz RECREATE=1 -# lub: bash scripts/pg-collation-migrate-3-load.sh <…-nocollation.sql.gz> --recreate-volume +make migrate-collation-load SQL=/…/backups/db-backup-20260613-190000-nocollation.sql RECREATE=1 +# lub: bash scripts/pg-collation-migrate-3-load.sh <…-nocollation.sql> --recreate-volume ``` `--recreate-volume` zatrzyma aplikację + dbserver, **usunie** volume `${COMPOSE_PROJECT_NAME}_postgresql_data` (DESTRUKCYJNE — pyta o potwierdzenie!), wstanie dbserver na nowym obrazie (initdb z ICU pl-PL), po czym `dropdb`+ -`createdb`+`psql -f`. Bez `--recreate-volume` zakłada, że dbserver już chodzi na -stockowym obrazie na pustym volume. +`createdb`+`psql` (z paskiem `pv`, jeśli jest). Bez `--recreate-volume` zakłada, +że dbserver już chodzi na stockowym obrazie na pustym volume. ### 4. Domigruj i wstań @@ -114,13 +117,19 @@ make dbshell-psql # potem: ## Uwagi i ograniczenia -- **Cały pipeline jest plain SQL** (dump `-Fp` → `sed` → `psql -f`), bo kolację - trzeba wyciąć z **tekstu** definicji widoków (`COLLATE "pl_PL"`), czego format - katalogowy/custom (binarny `toc.dat`) nie pozwala zrobić bez konwersji +- **Cały pipeline jest plain SQL, bez gzipa** (dump `-Fp` → `sed` → `psql`), bo + kolację trzeba wyciąć z **tekstu** definicji widoków (`COLLATE pl_PL`), czego + format katalogowy/custom (binarny `toc.dat`) nie pozwala zrobić bez konwersji `pg_restore -f -`. Load i tak jest jednowątkowy (`psql`), więc równoległość `pg_restore -Fd -j` nic by tu nie dała — dlatego żaden binarny pośrednik nie - jest potrzebny. Dla bardzo dużych baz dump/load jest sekwencyjny, ale to - operacja jednorazowa. + jest potrzebny. Brak (de)kompresji jest też trochę szybszy (kosztem miejsca na + nieskompresowany `.sql`). Dla bardzo dużych baz dump/load jest sekwencyjny, ale + to operacja jednorazowa. +- **Nazwa kolacji jest case-insensitive** (`[pP][lL]_[pP][lL]`), z opcjonalnym + `public.` i cudzysłowem. Realne bazy mają `public.pl_pl` (małe litery, + `locale='pl_PL.utf8'`), a nie `"pl_PL"` z `0001_collation` — wzorzec łapie oba. + Migracja bpp `0443` dropuje tylko `"pl_PL"`, więc dla ścieżki dump→restore to + `sed` (a nie migracja) gwarantuje usunięcie `pl_pl`. - **`make backup` wciąż działa ze starym obrazem** — obrazy `iplweb/bpp_dbserver` są nadal na Docker Hubie, więc dump da się zrobić nawet po decyzji o migracji. - Skrypt kroku 3 **odmówi** załadowania do kontenera nadal działającego na diff --git a/mk/database.mk b/mk/database.mk index 231feb9..9e0bb6e 100644 --- a/mk/database.mk +++ b/mk/database.mk @@ -180,8 +180,8 @@ test-upgrade-postgres: # na scripts/pg-collation-migrate-{1-dump,2-fix,3-load}.sh). Pelny opis: # docs/eksploatacja/migracja-collation-stock-pg.md. # make migrate-collation-dump [STOP_APP=1] [YES=1] -# make migrate-collation-fix DUMPGZ=/.../db-backup-*.sql.gz -# make migrate-collation-load SQLGZ=/.../*-nocollation.sql.gz [RECREATE=1] [YES=1] +# make migrate-collation-fix DUMPSQL=/.../db-backup-*.sql +# make migrate-collation-load SQL=/.../*-nocollation.sql [RECREATE=1] [YES=1] COLLATION_DUMP_FLAGS := ifdef STOP_APP COLLATION_DUMP_FLAGS += --stop-app @@ -194,11 +194,11 @@ migrate-collation-dump: @bash scripts/pg-collation-migrate-1-dump.sh $(COLLATION_DUMP_FLAGS) migrate-collation-fix: - @if [ -z "$(DUMPGZ)" ]; then \ - echo "Uzycie: make migrate-collation-fix DUMPGZ=/.../db-backup-YYYYMMDD-HHMMSS.sql.gz" >&2; \ + @if [ -z "$(DUMPSQL)" ]; then \ + echo "Uzycie: make migrate-collation-fix DUMPSQL=/.../db-backup-YYYYMMDD-HHMMSS.sql" >&2; \ exit 1; \ fi - @bash scripts/pg-collation-migrate-2-fix.sh "$(DUMPGZ)" + @bash scripts/pg-collation-migrate-2-fix.sh "$(DUMPSQL)" COLLATION_LOAD_FLAGS := ifdef RECREATE @@ -209,8 +209,8 @@ ifdef YES endif migrate-collation-load: - @if [ -z "$(SQLGZ)" ]; then \ - echo "Uzycie: make migrate-collation-load SQLGZ=/.../db-backup-...-nocollation.sql.gz [RECREATE=1]" >&2; \ + @if [ -z "$(SQL)" ]; then \ + echo "Uzycie: make migrate-collation-load SQL=/.../db-backup-...-nocollation.sql [RECREATE=1]" >&2; \ exit 1; \ fi - @bash scripts/pg-collation-migrate-3-load.sh "$(SQLGZ)" $(COLLATION_LOAD_FLAGS) + @bash scripts/pg-collation-migrate-3-load.sh "$(SQL)" $(COLLATION_LOAD_FLAGS) diff --git a/scripts/pg-collation-migrate-1-dump.sh b/scripts/pg-collation-migrate-1-dump.sh index 46b5329..161f473 100755 --- a/scripts/pg-collation-migrate-1-dump.sh +++ b/scripts/pg-collation-migrate-1-dump.sh @@ -5,16 +5,20 @@ # # Robi logical dump BIEZACEGO clustra (ktory wciaz chodzi na STARYM obrazie # iplweb/bpp_dbserver, bo tylko on ma locale libc pl_PL.UTF-8) do PLAIN SQL -# (pg_dump -Fp), spakowanego gzipem -> db-backup-.sql.gz. +# (pg_dump -Fp) -> db-backup-.sql. BEZ gzipa — krok 2 i tak musi czytac +# i edytowac caly tekst sed-em, a brak (de)kompresji jest troche szybszy. # # DLACZEGO plain SQL, a nie format katalogowy (-Fd) jak `make db-backup`: # krok 2 musi EDYTOWAC TEKST (sed) definicji widokow, zeby wyciac klauzule -# COLLATE "pl_PL". Format binarny (-Fd/-Fc) trzeba by najpierw skonwertowac +# COLLATE pl_PL. Format binarny (-Fd/-Fc) trzeba by najpierw skonwertowac # `pg_restore -f -` (dodatkowy obraz postgres na hoscie + tar/untar) — a # skoro load i tak idzie psql-em (jednowatkowo), rownoleglosc pg_restore -j -# nic nie daje. Plain SQL = zaden binarny posrednik. Wynikowy .sql.gz jest +# nic nie daje. Plain SQL = zaden binarny posrednik. Wynikowy .sql jest # tez normalnym, ladowalnym backupem. # +# Jesli na hoscie jest `pv`, dump pokazuje pasek postepu (estymata rozmiaru +# z pg_database_size). Bez pv leci bez paska — `pv` jest opcjonalne. +# # WAZNE: zanim zrzucisz, zatrzymaj zapisy. Domyslnie skrypt NIE zatrzymuje # aplikacji; podaj --stop-app, zeby zatrzymal appserver + workery + beat + # denorm-queue (jak `make restore`). Alternatywnie zrob recznie: @@ -23,7 +27,7 @@ # Uzycie: # bash scripts/pg-collation-migrate-1-dump.sh [--stop-app] [--yes] # -# Wypisuje na STDOUT pelna sciezke do .sql.gz (do podania krokowi 2). +# Wypisuje na STDOUT pelna sciezke do .sql (do podania krokowi 2). set -euo pipefail REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" @@ -32,6 +36,9 @@ REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" STOP_APP=0 NOINPUT=0 +# NOINPUT czyta confirm() z sourcowanej lib; shellcheck bez -x tego uzycia nie +# widzi -> SC2034. Nie eksportujemy (to flaga sterujaca, nie env dla pg_dump/psql). +# shellcheck disable=SC2034 while [ $# -gt 0 ]; do case "$1" in --stop-app) STOP_APP=1; shift ;; @@ -44,8 +51,8 @@ done load_env TS="$(date +%Y%m%d-%H%M%S)" -DUMP_SQL_GZ="db-backup-${TS}.sql.gz" -OUT_PATH="${HOST_BACKUP_DIR}/${DUMP_SQL_GZ}" +DUMP_SQL="db-backup-${TS}.sql" +OUT_PATH="${HOST_BACKUP_DIR}/${DUMP_SQL}" PARTIAL="${OUT_PATH}.partial" # Niedokonczony zrzut (pg_dump padl w polowie potoku) nie moze udawac @@ -67,22 +74,49 @@ else confirm "Kontynuowac zrzut bez zatrzymania aplikacji?" || { echo "Przerwano."; exit 1; } fi -# pg_dump -Fp pisze czysty SQL na stdout (in-container), gzip pakuje na -# hoscie. Brak -j (plain SQL nie wspiera rownoleglosci) i tak nie jest -# strata: load w kroku 3 to jednowatkowy psql. -T wylacza pseudo-TTY, wiec -# strumien nie jest psuty translacja CR/LF. pipefail (z set -o) wylapie -# blad pg_dump mimo gzipa po prawej stronie potoku. -echo ">> pg_dump -Fp (zrodlo: stary cluster) | gzip -> ${OUT_PATH}" >&2 +# Pasek postepu: jesli na hoscie jest `pv` i stderr to terminal, wpinamy go +# miedzy pg_dump a plik. Jako estymate (-s) bierzemy fizyczny rozmiar bazy +# (pg_database_size) — to tylko przyblizenie (dump nie ma indeksow ani bloatu, +# wiec zwykle konczy przed 100%), ale daje procent + ETA. Gdy pv nie ma, +# identity-pipe przez `cat`. Nieudane zapytanie o rozmiar -> pv bez -s. +PROGRESS=(cat) +if [ -t 2 ] && command -v pv >/dev/null 2>&1; then + DB_BYTES="$(dc exec -T -e "PGPASSWORD=${DJANGO_BPP_DB_PASSWORD}" dbserver psql \ + -h "${DJANGO_BPP_DB_HOST}" -p "${DJANGO_BPP_DB_PORT}" \ + -U "${DJANGO_BPP_DB_USER}" -d "${DJANGO_BPP_DB_NAME}" \ + -tAc "SELECT pg_database_size('${DJANGO_BPP_DB_NAME}')" 2>/dev/null \ + | tr -d '[:space:]')" + case "$DB_BYTES" in ''|*[!0-9]*) DB_BYTES=0 ;; esac + if [ "$DB_BYTES" -gt 0 ]; then + PROGRESS=(pv -s "$DB_BYTES") + else + PROGRESS=(pv) + fi +elif ! command -v pv >/dev/null 2>&1; then + echo ">> (bez paska postepu — zainstaluj 'pv': apt install pv / brew install pv)" >&2 +fi + +# pg_dump -Fp pisze czysty SQL na stdout (in-container), zapisujemy go wprost +# do pliku na hoscie (bez gzipa). Brak -j (plain SQL nie wspiera rownoleglosci) +# i tak nie jest strata: load w kroku 3 to jednowatkowy psql. -T wylacza +# pseudo-TTY, wiec strumien nie jest psuty translacja CR/LF. pipefail (z set -o) +# wylapie blad pg_dump mimo pv po prawej stronie potoku. +echo ">> pg_dump -Fp (zrodlo: stary cluster) -> ${OUT_PATH}" >&2 dc exec -T -e "PGPASSWORD=${DJANGO_BPP_DB_PASSWORD}" dbserver pg_dump \ -Fp \ -h "${DJANGO_BPP_DB_HOST}" -p "${DJANGO_BPP_DB_PORT}" \ -U "${DJANGO_BPP_DB_USER}" "${DJANGO_BPP_DB_NAME}" \ - | gzip > "$PARTIAL" + | "${PROGRESS[@]}" > "$PARTIAL" -echo ">> Sprawdzam integralnosc gzipa..." >&2 -gzip -t "$PARTIAL" +# Kompletnosc: pg_dump -Fp konczy zrzut markerem na koncu pliku. Jego brak = +# zrzut urwany (zamiast `gzip -t`, ktory mielismy przy wersji gzipowanej). +echo ">> Sprawdzam kompletnosc zrzutu (marker konca pg_dump)..." >&2 +if ! tail -n 5 "$PARTIAL" | grep -q 'PostgreSQL database dump complete'; then + echo "BLAD: zrzut wyglada na urwany (brak markera 'PostgreSQL database dump complete')." >&2 + exit 1 +fi mv "$PARTIAL" "$OUT_PATH" echo ">> Gotowe. Zrzut plain SQL:" >&2 -# Jedyne, co idzie na czysty STDOUT — sciezka do .sql.gz (dla kroku 2). +# Jedyne, co idzie na czysty STDOUT — sciezka do .sql (dla kroku 2). printf '%s\n' "$OUT_PATH" diff --git a/scripts/pg-collation-migrate-2-fix.sh b/scripts/pg-collation-migrate-2-fix.sh index e427299..9f3368b 100755 --- a/scripts/pg-collation-migrate-2-fix.sh +++ b/scripts/pg-collation-migrate-2-fix.sh @@ -3,19 +3,25 @@ # KROK 2/3 migracji "pozbadz sie kolacji libc pl_PL". Patrz # lib-pg-collation-migrate.sh. # -# Bierze zrzut plain SQL (db-backup-.sql.gz z kroku 1) i WYCINA z niego +# Bierze zrzut plain SQL (db-backup-.sql z kroku 1) i WYCINA z niego # kolacje pl_PL czystym sed-em: -# * usuwa CREATE/ALTER/COMMENT ... COLLATION ... "pl_PL" -# * usuwa klauzule COLLATE [public.]"pl_PL" (z 5 widokow bpp_kronika_*) -# Wynik: -nocollation.sql.gz w katalogu backupow — wejscie kroku 3. +# * usuwa CREATE/ALTER/COMMENT ... COLLATION ... pl_PL +# * usuwa naglowkowy komentarz pg_dump "-- Name: pl_PL...; Type: COLLATION" +# * usuwa klauzule COLLATE [public.]pl_PL (z widokow bpp_kronika_*) +# Wynik: -nocollation.sql w katalogu backupow — wejscie kroku 3. # -# To czysta transformacja tekstu na hoscie: gunzip | sed | gzip. Zaden -# pg_restore, zaden obraz postgres, zaden tar — bo krok 1 daje juz plain -# SQL (patrz lib: kolacja siedzi w TEKSCIE definicji widokow, a tego nie -# da sie wyciac z binarnego -Fd inaczej niz konwertujac go wpierw na SQL). +# WAZNE — nazwa kolacji jest case-insensitive i moze byc w cudzyslowie albo +# bez. Realne bazy maja `public.pl_pl` (male litery, bez cudzyslowu, +# locale='pl_PL.utf8') ORAZ/LUB `public."pl_PL"` (0001_collation). Dlatego +# wzorzec uzywa klas znakowych [pP][lL]_[pP][lL] i opcjonalnego cudzyslowu — +# lapie KAZDY wariant. (Wczesniejsza wersja matchowala tylko `pl_PL` i +# przepuszczala `pl_pl` -> load padal na "could not create locale pl_PL.utf8".) +# +# To czysta transformacja tekstu na hoscie: sed in -> out, bez gzipa, bez +# pg_restore, bez obrazu postgres, bez tar (krok 1 daje juz plain SQL). # # Uzycie: -# bash scripts/pg-collation-migrate-2-fix.sh +# bash scripts/pg-collation-migrate-2-fix.sh set -euo pipefail REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" @@ -23,54 +29,55 @@ REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" . "$REPO_DIR/scripts/lib-pg-collation-migrate.sh" if [ $# -lt 1 ] || [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then - sed -n '2,21p' "$0"; exit 0 + sed -n '2,27p' "$0"; exit 0 fi -SRC_GZ="$1" -[ -f "$SRC_GZ" ] || { echo "BLAD: nie ma pliku: $SRC_GZ" >&2; exit 1; } -SRC_GZ="$(cd "$(dirname "$SRC_GZ")" && pwd)/$(basename "$SRC_GZ")" +SRC_SQL="$1" +[ -f "$SRC_SQL" ] || { echo "BLAD: nie ma pliku: $SRC_SQL" >&2; exit 1; } +SRC_SQL="$(cd "$(dirname "$SRC_SQL")" && pwd)/$(basename "$SRC_SQL")" -# Lapiemy stary, binarny format z poprzedniej wersji skryptu (-Fd tarball). -case "$SRC_GZ" in - *.tar.gz|*.tgz) - echo "BLAD: to wyglada na tarball (-Fd). Krok 1 produkuje teraz plain" >&2 - echo " SQL (db-backup-.sql.gz) — podaj plik .sql.gz." >&2 +# Lapiemy stare formaty z poprzednich wersji (-Fd tarball / gzipowany SQL). +case "$SRC_SQL" in + *.tar.gz|*.tgz|*.gz) + echo "BLAD: krok 1 produkuje teraz NIESKOMPRESOWANY plain SQL" >&2 + echo " (db-backup-.sql) — podaj plik .sql, nie .gz/.tar.gz." >&2 exit 1 ;; esac -BASE="$(basename "$SRC_GZ")"; BASE="${BASE%.sql.gz}"; BASE="${BASE%.gz}" -OUT_DIR="$(dirname "$SRC_GZ")" -OUT_SQL_GZ="${OUT_DIR}/${BASE}-nocollation.sql.gz" +BASE="$(basename "$SRC_SQL")"; BASE="${BASE%.sql}" +OUT_DIR="$(dirname "$SRC_SQL")" +OUT_SQL="${OUT_DIR}/${BASE}-nocollation.sql" -echo ">> Sprawdzam integralnosc wejsciowego gzipa..." >&2 -gzip -t "$SRC_GZ" +# Nazwa kolacji: opcjonalne `public.`, opcjonalny cudzyslow, dowolny case. +# (W double-quotes ponizej cudzyslowy z $NAME trafiaja do seda doslownie.) +NAME='(public\.)?"?[pP][lL]_[pP][lL]"?' -echo ">> gunzip | sed (wycinam kolacje pl_PL) | gzip -> ${OUT_SQL_GZ##*/}" >&2 -gunzip -c "$SRC_GZ" \ - | sed -E \ - -e '/^CREATE COLLATION (public\.)?"?pl_PL"?/d' \ - -e '/^ALTER COLLATION (public\.)?"?pl_PL"?/d' \ - -e '/^COMMENT ON COLLATION (public\.)?"?pl_PL"?/d' \ - -e 's/ COLLATE (public\.)?"pl_PL"//g' \ - | gzip > "$OUT_SQL_GZ" +echo ">> sed (wycinam kolacje pl_PL, dowolny case) -> ${OUT_SQL##*/}" >&2 +sed -E \ + -e "/^CREATE COLLATION ${NAME} /d" \ + -e "/^ALTER COLLATION ${NAME} /d" \ + -e "/^COMMENT ON COLLATION ${NAME} /d" \ + -e "/^-- Name: [pP][lL]_[pP][lL].*Type: COLLATION/d" \ + -e "s/ COLLATE ${NAME}//g" \ + "$SRC_SQL" > "$OUT_SQL" +# Weryfikacja: zadnych aktywnych odwolan do kolacji pl_PL (dowolny case). echo ">> Weryfikacja: zadnych pozostalosci kolacji pl_PL w wyniku..." >&2 -if zgrep -nE 'COLLATION[[:space:]]+("?public"?\.)?"?pl_PL|COLLATE[[:space:]]+(public\.)?"pl_PL"' \ - "$OUT_SQL_GZ" >/dev/null 2>&1; then +RESIDUAL_RE='(CREATE|ALTER|COMMENT ON) COLLATION[[:space:]]+(public\.)?"?[pP][lL]_[pP][lL]|COLLATE[[:space:]]+(public\.)?"?[pP][lL]_[pP][lL]' +if grep -nE "$RESIDUAL_RE" "$OUT_SQL" >/dev/null 2>&1; then echo "BLAD: w wyniku nadal sa odwolania do kolacji pl_PL:" >&2 - zgrep -nE 'COLLATION[[:space:]]+("?public"?\.)?"?pl_PL|COLLATE[[:space:]]+(public\.)?"pl_PL"' \ - "$OUT_SQL_GZ" | head >&2 - rm -f "$OUT_SQL_GZ" + grep -nE "$RESIDUAL_RE" "$OUT_SQL" | head >&2 + rm -f "$OUT_SQL" exit 1 fi # Sanity: zrzut sprzed migracji bpp 0442 ma jeszcze plpython3u, ktorego stock # postgres nie ma -> load padnie. Ostrzegamy (nie blokujemy). -if zgrep -qiE 'plpython3u|LANGUAGE plpython' "$OUT_SQL_GZ"; then +if grep -qiE 'plpython3u|LANGUAGE plpython' "$OUT_SQL"; then echo "!! OSTRZEZENIE: zrzut zawiera plpython3u (sprzed migracji bpp 0442)." >&2 echo " Stock postgres go nie ma -> krok 3 padnie. Zrob NOWY zrzut po" >&2 echo " wdrozeniu wersji aplikacji z migracja 0442 (DROP EXTENSION plpython3u)." >&2 fi echo ">> Gotowe. Poprawiony zrzut:" >&2 -printf '%s\n' "$OUT_SQL_GZ" +printf '%s\n' "$OUT_SQL" diff --git a/scripts/pg-collation-migrate-3-load.sh b/scripts/pg-collation-migrate-3-load.sh index 9ba0477..e4985a3 100755 --- a/scripts/pg-collation-migrate-3-load.sh +++ b/scripts/pg-collation-migrate-3-load.sh @@ -3,8 +3,9 @@ # KROK 3/3 migracji "pozbadz sie kolacji libc pl_PL". Patrz # lib-pg-collation-migrate.sh. # -# Laduje poprawiony zrzut (<...>-nocollation.sql.gz z kroku 2) do SWIEZEGO -# clustra na STOCKOWYM obrazie postgres (np. 18), zainicjowanego z kolacja +# Laduje poprawiony zrzut (<...>-nocollation.sql z kroku 2, plain SQL bez +# gzipa) do SWIEZEGO clustra na STOCKOWYM obrazie postgres (np. 18), +# zainicjowanego z kolacja # ICU pl-PL (POSTGRES_INITDB_ARGS=--locale-provider=icu --icu-locale=pl-PL, # ustawiane w docker-compose.database.yml). # @@ -17,7 +18,7 @@ # i upewnij sie, ze docker-compose.database.yml uzywa stockowego postgres. # # Uzycie: -# bash scripts/pg-collation-migrate-3-load.sh \ +# bash scripts/pg-collation-migrate-3-load.sh \ # [--recreate-volume] [--yes] set -euo pipefail @@ -25,23 +26,32 @@ REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" # shellcheck source=scripts/lib-pg-collation-migrate.sh . "$REPO_DIR/scripts/lib-pg-collation-migrate.sh" -SQL_GZ="" +SQL_FILE="" RECREATE=0 NOINPUT=0 +# NOINPUT czyta confirm() z sourcowanej lib; shellcheck bez -x tego uzycia nie +# widzi -> SC2034. Nie eksportujemy (to flaga sterujaca, nie env dla procesow). +# shellcheck disable=SC2034 while [ $# -gt 0 ]; do case "$1" in --recreate-volume) RECREATE=1; shift ;; --yes|--noinput|--non-interactive) NOINPUT=1; shift ;; - -h|--help) sed -n '2,22p' "$0"; exit 0 ;; + -h|--help) sed -n '2,23p' "$0"; exit 0 ;; -*) echo "BLAD: nieznany argument: $1" >&2; exit 1 ;; - *) SQL_GZ="$1"; shift ;; + *) SQL_FILE="$1"; shift ;; esac done -if [ -z "$SQL_GZ" ] || [ ! -f "$SQL_GZ" ]; then - echo "BLAD: podaj istniejacy plik *-nocollation.sql.gz" >&2 +if [ -z "$SQL_FILE" ] || [ ! -f "$SQL_FILE" ]; then + echo "BLAD: podaj istniejacy plik *-nocollation.sql (plain SQL z kroku 2)" >&2 exit 1 fi -SQL_GZ="$(cd "$(dirname "$SQL_GZ")" && pwd)/$(basename "$SQL_GZ")" +case "$SQL_FILE" in + *.gz|*.tgz) + echo "BLAD: krok 2 produkuje teraz NIESKOMPRESOWANY .sql — podaj plik" >&2 + echo " *-nocollation.sql, nie .gz." >&2 + exit 1 ;; +esac +SQL_FILE="$(cd "$(dirname "$SQL_FILE")" && pwd)/$(basename "$SQL_FILE")" load_env @@ -94,13 +104,22 @@ if [ "${PROV:-}" != "i" ]; then echo " POSTGRES_INITDB_ARGS=--locale-provider=icu --icu-locale=pl-PL ?" >&2 fi -echo ">> Laduje ${SQL_GZ} (psql, ON_ERROR_STOP=1)..." >&2 -gunzip -c "$SQL_GZ" | "${PSQL[@]}" -v ON_ERROR_STOP=1 -d "${DJANGO_BPP_DB_NAME}" +echo ">> Laduje ${SQL_FILE} (psql, ON_ERROR_STOP=1)..." >&2 +# Plain SQL z pliku -> psql w kontenerze (dc exec -T forwarduje stdin). Gdy +# jest `pv` i stderr to terminal, pokazujemy pasek postepu (pv sam zna rozmiar +# pliku). pipefail (set -o) wylapie blad psql mimo pv po lewej stronie potoku. +if [ -t 2 ] && command -v pv >/dev/null 2>&1; then + pv "$SQL_FILE" | "${PSQL[@]}" -v ON_ERROR_STOP=1 -d "${DJANGO_BPP_DB_NAME}" +else + "${PSQL[@]}" -v ON_ERROR_STOP=1 -d "${DJANGO_BPP_DB_NAME}" < "$SQL_FILE" +fi echo ">> Weryfikacja po loadzie:" >&2 "${PSQL[@]}" -d "${DJANGO_BPP_DB_NAME}" -tAc \ "SELECT 'kronika views: '||count(*) FROM information_schema.views WHERE table_name LIKE 'bpp_kronika%';" >&2 +# Dowolny case (pl_PL/pl_pl) w schemacie public; systemowa pg_catalog.pl_PL +# (auto-import initdb) jest pomijana przez nspname='public'. "${PSQL[@]}" -d "${DJANGO_BPP_DB_NAME}" -tAc \ - "SELECT 'collations pl_PL pozostale: '||count(*) FROM pg_collation WHERE collname='pl_PL';" >&2 + "SELECT 'kolacje pl_PL w public pozostale: '||count(*) FROM pg_collation c JOIN pg_namespace n ON n.oid=c.collnamespace WHERE lower(c.collname)='pl_pl' AND n.nspname='public';" >&2 echo ">> Gotowe. Baza '${DJANGO_BPP_DB_NAME}' zaladowana na ${IMG}." >&2 echo " Teraz: zmigruj aplikacje ('make migrate') i wstan stack ('make up')." >&2 From 11ca23d2e7a91883efdf569f4bb3395c039cf21a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sat, 13 Jun 2026 21:47:28 +0200 Subject: [PATCH 05/15] fix(collation-migrate): match quoted collation name with dot suffix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Realny prod-dump ma nazwe kolacji `public."pl_PL.utf8"` — identyfikator z kropka i `.utf8` W SRODKU cudzyslowu (musi byc cytowany). Poprzedni wzorzec `"?pl_PL"?` wymagal spacji/cudzyslowu zaraz po `pl_PL`, wiec `.utf8` go omijal -> CREATE/ALTER COLLATION przechodzily, load padal. Wzorzec nazwy = (public.)? ( "pl_PL" | goly pl_pl ): QNAME='"[pP][lL]_[pP][lL][^"]*"' # "pl_PL.utf8" / "pl_PL.UTF-8" / "pl_PL" BNAME='[pP][lL]_[pP][lL]' # pl_pl (niecytowany ident nie ma kropek) Oddzielne galezie quoted/unquoted chronia przed zjedzeniem przecinka w `COLLATE pl_pl, y` (goly ident konczy sie po 5 znakach, nie lyka `,`). Zweryfikowane na fixturze: "pl_PL.utf8"/"pl_PL"/pl_pl + 3 formy COLLATE -> 0; przecinek w `(x COLLATE public.pl_pl), y` zachowany; '0443_drop_pl_PL_collation' w django_migrations i string-mention 'pl_PL' nietkniete. shellcheck Passed. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/pg-collation-migrate-2-fix.sh | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/scripts/pg-collation-migrate-2-fix.sh b/scripts/pg-collation-migrate-2-fix.sh index 9f3368b..fd40d5d 100755 --- a/scripts/pg-collation-migrate-2-fix.sh +++ b/scripts/pg-collation-migrate-2-fix.sh @@ -48,11 +48,17 @@ BASE="$(basename "$SRC_SQL")"; BASE="${BASE%.sql}" OUT_DIR="$(dirname "$SRC_SQL")" OUT_SQL="${OUT_DIR}/${BASE}-nocollation.sql" -# Nazwa kolacji: opcjonalne `public.`, opcjonalny cudzyslow, dowolny case. -# (W double-quotes ponizej cudzyslowy z $NAME trafiaja do seda doslownie.) -NAME='(public\.)?"?[pP][lL]_[pP][lL]"?' +# Nazwa kolacji: opcjonalne `public.`, potem ALBO nazwa w cudzyslowie z +# dowolnym sufiksem — realnie `"pl_PL.utf8"` (kropka + .utf8 W SRODKU +# cudzyslowu, prod 2026-06-13), ale tez `"pl_PL.UTF-8"` / `"pl_PL"` — ALBO +# goly identyfikator `pl_pl` (bez kropki, bo niecytowany ident nie ma kropek). +# Dowolny case przez klasy [pP][lL]. Cudzyslowy z $NAME trafiaja do seda +# doslownie — to KLUCZOWE dla nazw z kropka, ktore MUSZA byc cytowane. +QNAME='"[pP][lL]_[pP][lL][^"]*"' +BNAME='[pP][lL]_[pP][lL]' +NAME="(public\\.)?(${QNAME}|${BNAME})" -echo ">> sed (wycinam kolacje pl_PL, dowolny case) -> ${OUT_SQL##*/}" >&2 +echo ">> sed (wycinam kolacje pl_PL: \"pl_PL.utf8\"/pl_pl/..., dowolny case) -> ${OUT_SQL##*/}" >&2 sed -E \ -e "/^CREATE COLLATION ${NAME} /d" \ -e "/^ALTER COLLATION ${NAME} /d" \ @@ -62,8 +68,10 @@ sed -E \ "$SRC_SQL" > "$OUT_SQL" # Weryfikacja: zadnych aktywnych odwolan do kolacji pl_PL (dowolny case). +# Nie zlapie stringa '0443_drop_pl_PL_collation' z django_migrations (brak +# ^CREATE COLLATION / ` COLLATE ` przed nazwa). echo ">> Weryfikacja: zadnych pozostalosci kolacji pl_PL w wyniku..." >&2 -RESIDUAL_RE='(CREATE|ALTER|COMMENT ON) COLLATION[[:space:]]+(public\.)?"?[pP][lL]_[pP][lL]|COLLATE[[:space:]]+(public\.)?"?[pP][lL]_[pP][lL]' +RESIDUAL_RE='^(CREATE|ALTER|COMMENT ON) COLLATION (public\.)?"?[pP][lL]_[pP][lL]| COLLATE (public\.)?"?[pP][lL]_[pP][lL]' if grep -nE "$RESIDUAL_RE" "$OUT_SQL" >/dev/null 2>&1; then echo "BLAD: w wyniku nadal sa odwolania do kolacji pl_PL:" >&2 grep -nE "$RESIDUAL_RE" "$OUT_SQL" | head >&2 From def69d496d295edea3f032f7e6a836f31e547262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sat, 13 Jun 2026 22:12:24 +0200 Subject: [PATCH 06/15] fix(collation-migrate): step 1 --no-owner --no-privileges; step 2 stays collation-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Load do swiezego klastra padal na "role bpp does not exist": per-bazowy pg_dump nie zrzuca rol (sa cluster-level), a swiezy psql 18 ma tylko superusera POSTGRES_USER (np. postgres), nie ma `bpp` z OWNER TO. Rozwiazanie strukturalne i bezpieczne: krok 1 robi teraz `pg_dump --no-owner --no-privileges` — zrzut bez OWNER TO / GRANT / REVOKE, wiec laduje sie do dowolnego klastra (obiekty obejmuje user ladujacy). Pojedynczy pg_dump i tak nie tworzy userow (CREATE ROLE to pg_dumpall). Krok 2 NIE strippuje ownerow/grantow sed-em: rozwazane, ale `^GRANT`/ `^OWNER TO` potrafi wystapic w ciele funkcji (dollar-quote) i line-based sed by je uszkodzil. Owner-stripping nalezy do dump-time (--no-owner), nie do post-processingu. Krok 2 zostaje czysto kolacyjny. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/pg-collation-migrate-1-dump.sh | 12 ++++++++++-- scripts/pg-collation-migrate-2-fix.sh | 19 ++++++++++++------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/scripts/pg-collation-migrate-1-dump.sh b/scripts/pg-collation-migrate-1-dump.sh index 161f473..f03aaf9 100755 --- a/scripts/pg-collation-migrate-1-dump.sh +++ b/scripts/pg-collation-migrate-1-dump.sh @@ -16,6 +16,14 @@ # nic nie daje. Plain SQL = zaden binarny posrednik. Wynikowy .sql jest # tez normalnym, ladowalnym backupem. # +# --no-owner --no-privileges: zrzut bez `ALTER ... OWNER TO ` i bez +# GRANT/REVOKE. Docelowy SWIEZY cluster ma tylko superusera POSTGRES_USER +# (np. `postgres`) i NIE ma ról ze starego klastra (np. `bpp`) — per-bazowy +# pg_dump ról nie zrzuca (sa cluster-level). Bez tych flag load padal na +# "role bpp does not exist". Obiekty obejmuje na wlasnosc user ladujacy, +# Django laczy sie jako ten sam superuser. (Pojedynczy pg_dump i tak NIGDY +# nie tworzy userow — CREATE ROLE robi tylko pg_dumpall --globals.) +# # Jesli na hoscie jest `pv`, dump pokazuje pasek postepu (estymata rozmiaru # z pg_database_size). Bez pv leci bez paska — `pv` jest opcjonalne. # @@ -101,9 +109,9 @@ fi # i tak nie jest strata: load w kroku 3 to jednowatkowy psql. -T wylacza # pseudo-TTY, wiec strumien nie jest psuty translacja CR/LF. pipefail (z set -o) # wylapie blad pg_dump mimo pv po prawej stronie potoku. -echo ">> pg_dump -Fp (zrodlo: stary cluster) -> ${OUT_PATH}" >&2 +echo ">> pg_dump -Fp --no-owner --no-privileges (zrodlo: stary cluster) -> ${OUT_PATH}" >&2 dc exec -T -e "PGPASSWORD=${DJANGO_BPP_DB_PASSWORD}" dbserver pg_dump \ - -Fp \ + -Fp --no-owner --no-privileges \ -h "${DJANGO_BPP_DB_HOST}" -p "${DJANGO_BPP_DB_PORT}" \ -U "${DJANGO_BPP_DB_USER}" "${DJANGO_BPP_DB_NAME}" \ | "${PROGRESS[@]}" > "$PARTIAL" diff --git a/scripts/pg-collation-migrate-2-fix.sh b/scripts/pg-collation-migrate-2-fix.sh index fd40d5d..ccec361 100755 --- a/scripts/pg-collation-migrate-2-fix.sh +++ b/scripts/pg-collation-migrate-2-fix.sh @@ -11,11 +11,16 @@ # Wynik: -nocollation.sql w katalogu backupow — wejscie kroku 3. # # WAZNE — nazwa kolacji jest case-insensitive i moze byc w cudzyslowie albo -# bez. Realne bazy maja `public.pl_pl` (male litery, bez cudzyslowu, -# locale='pl_PL.utf8') ORAZ/LUB `public."pl_PL"` (0001_collation). Dlatego -# wzorzec uzywa klas znakowych [pP][lL]_[pP][lL] i opcjonalnego cudzyslowu — -# lapie KAZDY wariant. (Wczesniejsza wersja matchowala tylko `pl_PL` i -# przepuszczala `pl_pl` -> load padal na "could not create locale pl_PL.utf8".) +# bez. Realne bazy maja `public."pl_PL.utf8"` (cytowana, z kropka i .utf8 w +# srodku), albo `public.pl_pl` (male litery), albo `public."pl_PL"` +# (0001_collation). Wzorzec: (public.)? ( "pl_PL" | goly pl_pl ), +# dowolny case przez klasy [pP][lL] — lapie KAZDY wariant. +# +# UWAGA: ownerow i przywilejow (OWNER TO / GRANT / REVOKE) ten krok NIE +# rusza. Zrzut z kroku 1 robi `pg_dump --no-owner --no-privileges`, wiec rol +# nie ma juz w tekscie (per-bazowy pg_dump i tak nie zrzuca rol — sa cluster- +# level — przez co load do swiezego klastra padalby na "role ... does not +# exist"). Jesli masz STARY zrzut sprzed tej flagi: zrob nowy dump kroku 1. # # To czysta transformacja tekstu na hoscie: sed in -> out, bez gzipa, bez # pg_restore, bez obrazu postgres, bez tar (krok 1 daje juz plain SQL). @@ -29,7 +34,7 @@ REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" . "$REPO_DIR/scripts/lib-pg-collation-migrate.sh" if [ $# -lt 1 ] || [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then - sed -n '2,27p' "$0"; exit 0 + sed -n '2,29p' "$0"; exit 0 fi SRC_SQL="$1" @@ -70,7 +75,7 @@ sed -E \ # Weryfikacja: zadnych aktywnych odwolan do kolacji pl_PL (dowolny case). # Nie zlapie stringa '0443_drop_pl_PL_collation' z django_migrations (brak # ^CREATE COLLATION / ` COLLATE ` przed nazwa). -echo ">> Weryfikacja: zadnych pozostalosci kolacji pl_PL w wyniku..." >&2 +echo ">> Weryfikacja: brak pozostalosci kolacji pl_PL w wyniku..." >&2 RESIDUAL_RE='^(CREATE|ALTER|COMMENT ON) COLLATION (public\.)?"?[pP][lL]_[pP][lL]| COLLATE (public\.)?"?[pP][lL]_[pP][lL]' if grep -nE "$RESIDUAL_RE" "$OUT_SQL" >/dev/null 2>&1; then echo "BLAD: w wyniku nadal sa odwolania do kolacji pl_PL:" >&2 From 46c395e2f7ddb541c1a278a1805dd750140aa10f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sat, 13 Jun 2026 22:30:29 +0200 Subject: [PATCH 07/15] feat(restore): detekcja formatu zrzutu DB + --db-file + pliki .sql w --pick restore.sh dzialal tylko na parach db+media w formacie -Fd (.tar.gz) i globowal *.tar.gz, wiec plain .sql/.sql.gz nie dalo sie ani podac, ani wybrac przez --pick. Nowosci: - detekcja formatu zrzutu DB (auto): -Fd tar.gz (toc.dat) -> pg_restore -Fd; -Fc (magic PGDMP), tez gzipowany -> pg_restore; plain .sql / .sql.gz -> psql. pg_restore zawsze --no-owner --no-privileges; plain wczytywany psql-em. - --db-file=PATH: pojedynczy plik zrzutu (dowolny format, tez spoza katalogu backupow), tryb DB-only (media nietkniete). plain/.gz/-Fc ladowane przez stdin (dziala dla pliku spoza /backup); -Fd kopiowany do /backup gdy trzeba, nazwa katalogu top-level czytana z tarballa. - --db-format=auto|directory|custom|plain: reczny override detekcji. - --pick pokazuje TERAZ ZAROWNO pary db+media, JAK I pojedyncze zrzuty DB (*.sql/*.sql.gz/*.dump/*.custom). Wybor zwraca pair: albo file:. - Makefile: make restore DBFILE=... [DBFORMAT=...]. Zweryfikowane: detekcja 5 formatow na fixturach (directory/custom/custom-gz/ plain/plain-gz), listing pickera (pary + pliki, bez podwojnego listowania .tar.gz), ekstrakcja tagu, shellcheck Passed, -h OK. Sciezki dockerowe (pg_restore/psql/tar w kontenerze) do sprawdzenia na zywym stacku. Co-Authored-By: Claude Opus 4.8 (1M context) --- mk/database.mk | 12 +- scripts/restore.sh | 447 +++++++++++++++++++++++++++++++-------------- 2 files changed, 317 insertions(+), 142 deletions(-) diff --git a/mk/database.mk b/mk/database.mk index 9e0bb6e..f0e88c6 100644 --- a/mk/database.mk +++ b/mk/database.mk @@ -87,9 +87,11 @@ media-backup: # # Uzycie: # make restore # najnowsza para db+media, z safety-backup -# make restore PICK=1 # interaktywny wybor (fzf jesli jest) +# make restore PICK=1 # interaktywny wybor (pary ORAZ pliki .sql) # make restore TIMESTAMP=20260428-140218 # konkretna para -# make restore DB_ONLY=1 # tylko baza +# make restore DBFILE=/.../db.sql # pojedynczy plik DB (auto-detekcja formatu) +# make restore DBFILE=/.../db.dump DBFORMAT=custom # wymus format +# make restore DB_ONLY=1 # tylko baza (z pary) # make restore MEDIA_ONLY=1 # tylko media # make restore NO_SAFETY=1 # pomin safety-backup biezacego stanu # make restore YES=1 # noninteractive (auto-yes na confirm) @@ -97,6 +99,12 @@ RESTORE_FLAGS := ifdef TIMESTAMP RESTORE_FLAGS += --timestamp=$(TIMESTAMP) endif +ifdef DBFILE + RESTORE_FLAGS += --db-file=$(DBFILE) +endif +ifdef DBFORMAT + RESTORE_FLAGS += --db-format=$(DBFORMAT) +endif ifdef PICK RESTORE_FLAGS += --pick endif diff --git a/scripts/restore.sh b/scripts/restore.sh index 68da094..21b066a 100755 --- a/scripts/restore.sh +++ b/scripts/restore.sh @@ -1,28 +1,36 @@ #!/usr/bin/env bash # -# Disaster recovery / klonowanie srodowiska: wczytaj backup zrobiony przez -# `make backup` (lub `make backup-cycle`) z powrotem do uruchomionego stacka. +# Disaster recovery / klonowanie srodowiska: wczytaj backup z powrotem do +# uruchomionego stacka. # -# Strategia: -# 1. Wybor pary tarballi (db + media) o tym samym timestampie. -# 2. (Domyslnie) bezpieczny backup biezacego stanu jako fallback. -# 3. Stop appserver/workers/beat/denorm-queue. -# 4. dropdb + createdb + pg_restore -Fd -j N na bazie. -# 5. tar xzf media-backup na volume media. -# 6. make up. +# Zrodla zrzutu DB: +# - Para db+media po timestampie (`make backup`): db-backup-.tar.gz (-Fd) +# + media-backup-.tar.gz. Tryb domyslny (przywraca DB ORAZ media). +# - Pojedynczy plik DB dowolnego formatu: --db-file=PATH (tryb DB-only, +# media nietkniete). Plik moze byc poza katalogiem backupow. +# - --pick: interaktywny wybor — pokazuje ZAROWNO pary db+media, JAK I +# pojedyncze zrzuty DB (*.sql / *.sql.gz / *.dump / *.custom) z katalogu +# backupow. # -# Wybor backupu: -# - bez argumentow: najnowsza para db-backup-*.tar.gz + media-backup-*.tar.gz -# (parowanie po timestampie YYYYMMDD-HHMMSS w nazwie pliku). +# Detekcja formatu zrzutu DB (override: --db-format=auto|directory|custom|plain): +# - directory (-Fd, tar.gz z toc.dat) -> pg_restore -Fd -j N +# - custom (-Fc, magic PGDMP) -> pg_restore -Fc +# - plain (.sql, tez .sql.gz) -> psql +# pg_restore zawsze z --no-owner --no-privileges; plain SQL powinien byc +# dumpniety z --no-owner (patrz scripts/pg-collation-migrate-1-dump.sh) — +# inaczej load do swiezego klastra padnie na "role ... does not exist". +# +# Wybor backupu (tryb par): +# - bez argumentow: najnowsza para db-backup-*.tar.gz + media-backup-*.tar.gz. # - --timestamp=YYYYMMDD-HHMMSS: konkretna para. -# - --pick: interaktywny wybor (fzf jesli dostepne, fallback do numerowanego -# menu w czystym shellu). +# - --pick: interaktywny wybor (fzf jesli dostepne, inaczej numerowane menu). # # Wywolanie: `make restore` lub bezposrednio `bash scripts/restore.sh`. # -# UWAGA: ta operacja niszczy aktualna baze i nadpisuje volume media. Domyslnie -# robi safety-backup biezacego stanu PRZED restorem (mozna pominac flaga -# --no-safety-backup). Tarball safety-backupa zostaje w katalogu backupow. +# UWAGA: ta operacja niszczy aktualna baze i (w trybie par) nadpisuje volume +# media. Domyslnie robi safety-backup biezacego stanu PRZED restorem (mozna +# pominac flaga --no-safety-backup). Tarball safety-backupa zostaje w katalogu +# backupow. set -euo pipefail @@ -36,11 +44,17 @@ DB_ONLY=0 MEDIA_ONLY=0 SAFETY_BACKUP=1 NOINPUT=0 +DB_FILE="" +DB_FORMAT="auto" while [ $# -gt 0 ]; do case "$1" in --timestamp=*) TIMESTAMP_ARG="${1#*=}"; shift ;; --timestamp) TIMESTAMP_ARG="${2:-}"; shift 2 || { echo "BLAD: --timestamp wymaga wartosci." >&2; exit 1; } ;; + --db-file=*) DB_FILE="${1#*=}"; shift ;; + --db-file) DB_FILE="${2:-}"; shift 2 || { echo "BLAD: --db-file wymaga sciezki." >&2; exit 1; } ;; + --db-format=*) DB_FORMAT="${1#*=}"; shift ;; + --db-format) DB_FORMAT="${2:-}"; shift 2 || { echo "BLAD: --db-format wymaga wartosci." >&2; exit 1; } ;; --pick) PICK=1; shift ;; --db-only) DB_ONLY=1; shift ;; --media-only) MEDIA_ONLY=1; shift ;; @@ -50,29 +64,36 @@ while [ $# -gt 0 ]; do cat <<'HELP_EOF' Uzycie: restore.sh [OPCJE] - --timestamp=YYYYMMDD-HHMMSS Wybierz konkretna pare backupow (db + media). + Zrodlo DB: + --timestamp=YYYYMMDD-HHMMSS Para backupow (db + media) po timestampie. Domyslnie: najnowsza para w katalogu backupow. - - --pick Interaktywny wybor backupu. Uzywa fzf jesli - dostepne, w przeciwnym razie pokazuje - numerowane menu. - - --db-only Restore tylko bazy danych (pomija media). + --db-file=PATH Pojedynczy plik zrzutu DB (dowolny format, + tez spoza katalogu backupow). Tryb DB-only — + media NIE sa ruszane. + --pick Interaktywny wybor: pary db+media ORAZ + pojedyncze zrzuty DB (*.sql/*.sql.gz/*.dump). + + Format zrzutu DB: + --db-format=FMT auto (domyslnie) | directory | custom | plain. + auto wykrywa: -Fd tar.gz (toc.dat) -> pg_restore, + -Fc (PGDMP) -> pg_restore, .sql/.sql.gz -> psql. + + Zakres: + --db-only Restore tylko bazy (pomija media). --media-only Restore tylko mediow (pomija baze). - --no-safety-backup Pomija automatyczny backup biezacego stanu - przed restorem. NIE ZALECANE poza testami. - - --noinput, --yes Tryb nieinteraktywny - wszystkie potwierdzenia - auto-yes. Uzywaj swiadomie. - + --no-safety-backup Pomija auto-backup biezacego stanu przed + restorem. NIE ZALECANE poza testami. + --noinput, --yes Tryb nieinteraktywny (auto-yes na potwierdzenia). -h, --help Ten ekran. Przyklady: - bash scripts/restore.sh # najnowsza para, z safety - bash scripts/restore.sh --pick # interaktywny wybor - bash scripts/restore.sh --timestamp=20260428-140218 # konkretna para - bash scripts/restore.sh --db-only --no-safety-backup # tylko baza, bez safety + bash scripts/restore.sh # najnowsza para + bash scripts/restore.sh --pick # interaktywny wybor + bash scripts/restore.sh --timestamp=20260428-140218 # konkretna para + bash scripts/restore.sh --db-file=/backups/db.sql # plain SQL -> psql + bash scripts/restore.sh --db-file=/b/db.sql.gz --yes # .sql.gz -> psql + bash scripts/restore.sh --db-file=/b/db.dump # -Fc -> pg_restore HELP_EOF exit 0 ;; @@ -80,10 +101,19 @@ HELP_EOF esac done +case "$DB_FORMAT" in + auto|directory|custom|plain) : ;; + *) echo "BLAD: --db-format musi byc auto|directory|custom|plain (jest: $DB_FORMAT)" >&2; exit 1 ;; +esac + if [ "$DB_ONLY" = 1 ] && [ "$MEDIA_ONLY" = 1 ]; then echo "BLAD: --db-only i --media-only sa wzajemnie wykluczajace." >&2 exit 1 fi +if [ -n "$DB_FILE" ] && [ "$MEDIA_ONLY" = 1 ]; then + echo "BLAD: --db-file (DB-only) z --media-only nie ma sensu." >&2 + exit 1 +fi # ---- Helpers ------------------------------------------------------------ get_env_var() { @@ -110,12 +140,33 @@ confirm() { run() { echo "+ $*"; "$@"; } +# Thin wrapper na docker compose z jawnym plikiem (z include podciaga reszte). +dc() { docker compose -f "$REPO_DIR/docker-compose.yml" "$@"; } + fmt_size() { local f="$1" bytes bytes="$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f" 2>/dev/null || echo 0)" numfmt --to=iec --suffix=B "$bytes" 2>/dev/null || echo "${bytes}B" } +# Wykrywa format zrzutu DB. Echo: directory | custom | custom-gz | plain | plain-gz +# directory = -Fd (tar.gz z toc.dat) ; custom = -Fc (magic PGDMP) ; +# plain = czysty SQL. Wariant *-gz = to samo, ale skompresowane gzipem. +# Kolejnosc wazna: tar.gz tez przechodzi `gzip -t`, wiec tar sprawdzamy NAJPIERW. +detect_db_format() { + local f="$1" magic + if tar tzf "$f" 2>/dev/null | grep -q 'toc\.dat'; then + echo directory; return + fi + if gzip -t "$f" 2>/dev/null; then + magic="$(gunzip -c "$f" 2>/dev/null | head -c 5 || true)" + if [ "$magic" = "PGDMP" ]; then echo custom-gz; else echo plain-gz; fi + return + fi + magic="$(head -c 5 "$f" 2>/dev/null || true)" + if [ "$magic" = "PGDMP" ]; then echo custom; else echo plain; fi +} + # ---- Ladowanie zmiennych konfiguracyjnych ------------------------------ if [ ! -f "$REPO_ENV" ]; then echo "BLAD: brak $REPO_ENV. Najpierw uruchom 'make' zeby zainicjalizowac konfiguracje." >&2 @@ -161,11 +212,90 @@ fi PARALLEL_JOBS="${PARALLEL_JOBS:-4}" +# ---- Operacje na bazie (uzywane przez restore_db) ------------------------ +db_dropdb_createdb() { + echo "+ dropdb --force $DJANGO_BPP_DB_NAME" + dc exec -T -e "PGPASSWORD=$DJANGO_BPP_DB_PASSWORD" dbserver \ + dropdb --force \ + -h "$DJANGO_BPP_DB_HOST" -p "$DJANGO_BPP_DB_PORT" \ + -U "$DJANGO_BPP_DB_USER" "$DJANGO_BPP_DB_NAME" + echo "+ createdb $DJANGO_BPP_DB_NAME" + dc exec -T -e "PGPASSWORD=$DJANGO_BPP_DB_PASSWORD" dbserver \ + createdb \ + -h "$DJANGO_BPP_DB_HOST" -p "$DJANGO_BPP_DB_PORT" \ + -U "$DJANGO_BPP_DB_USER" "$DJANGO_BPP_DB_NAME" +} + +# psql czytajacy SQL ze stdin (plain / plain-gz po dekompresji na hoscie). +db_psql_stdin() { + dc exec -T -e "PGPASSWORD=$DJANGO_BPP_DB_PASSWORD" dbserver \ + psql -v ON_ERROR_STOP=1 \ + -h "$DJANGO_BPP_DB_HOST" -p "$DJANGO_BPP_DB_PORT" \ + -U "$DJANGO_BPP_DB_USER" -d "$DJANGO_BPP_DB_NAME" +} + +# pg_restore czytajacy archiwum -Fc ze stdin. +db_pgrestore_stdin() { + dc exec -T -e "PGPASSWORD=$DJANGO_BPP_DB_PASSWORD" dbserver \ + pg_restore --no-owner --no-privileges \ + -h "$DJANGO_BPP_DB_HOST" -p "$DJANGO_BPP_DB_PORT" \ + -U "$DJANGO_BPP_DB_USER" -d "$DJANGO_BPP_DB_NAME" +} + +# Restore -Fd (katalog) z tarballa. Wymaga, by tarball byl widoczny w +# kontenerze pod /backup (bind-mount HOST_BACKUP_DIR). Plik spoza tego +# katalogu kopiujemy tymczasowo. Nazwe katalogu top-level czytamy z tarballa. +restore_db_directory() { + local tar="$1" rel tmpcopy="" topdir + case "$tar" in + "$HOST_BACKUP_DIR"/*) rel="$(basename "$tar")" ;; + *) + tmpcopy="$HOST_BACKUP_DIR/.restore-dbfile-$$.tar.gz" + run cp "$tar" "$tmpcopy" + rel="$(basename "$tmpcopy")" + ;; + esac + topdir="$(dc exec -T dbserver tar tzf "/backup/$rel" 2>/dev/null \ + | head -1 | cut -d/ -f1 | tr -d '\r')" + if [ -z "$topdir" ]; then + echo "BLAD: nie udalo sie odczytac katalogu z tarballa $rel." >&2 + [ -n "$tmpcopy" ] && rm -f "$tmpcopy" + exit 1 + fi + run dc exec -T dbserver tar xzf "/backup/$rel" -C /backup + echo "+ pg_restore -Fd -j $PARALLEL_JOBS --no-owner --no-privileges" + dc exec -T -e "PGPASSWORD=$DJANGO_BPP_DB_PASSWORD" dbserver \ + pg_restore \ + -Fd -j "$PARALLEL_JOBS" \ + -h "$DJANGO_BPP_DB_HOST" -p "$DJANGO_BPP_DB_PORT" \ + -U "$DJANGO_BPP_DB_USER" -d "$DJANGO_BPP_DB_NAME" \ + --no-owner --no-privileges \ + "/backup/$topdir" + run dc exec -T dbserver rm -rf "/backup/$topdir" + [ -n "$tmpcopy" ] && rm -f "$tmpcopy" +} + +# Dropdb/createdb + restore wg formatu. +restore_db() { + local src="$1" fmt="$2" + echo + echo "=== DB restore (format: $fmt) z $(basename "$src") ===" + db_dropdb_createdb + case "$fmt" in + plain) db_psql_stdin < "$src" ;; + plain-gz) gunzip -c "$src" | db_psql_stdin ;; + custom) db_pgrestore_stdin < "$src" ;; + custom-gz) gunzip -c "$src" | db_pgrestore_stdin ;; + directory) restore_db_directory "$src" ;; + *) echo "BLAD: nieobslugiwany format DB: $fmt" >&2; exit 1 ;; + esac + echo "DB restore: OK" +} + # ---- Wybor backupu ------------------------------------------------------- list_timestamps() { - # Wypisuje unikalne timestampy ktore maja zarowno db-backup jak i media-backup - # (chyba ze --db-only / --media-only - wtedy tylko jeden typ jest wymagany). - # Sortuje malejaco (najnowszy pierwszy). + # Unikalne timestampy majace zarowno db-backup jak i media-backup + # (lub tylko jeden typ przy --db-only/--media-only). Sort malejaco. local db_ts media_ts db_ts="$(find "$HOST_BACKUP_DIR" -maxdepth 1 -type f -name 'db-backup-*.tar.gz' \ | sed 's|.*/db-backup-||; s|\.tar\.gz$||' | sort -r)" @@ -177,11 +307,17 @@ list_timestamps() { elif [ "$MEDIA_ONLY" = 1 ]; then printf '%s\n' "$media_ts" else - # Iloczyn - tylko timestampy obecne w obu zbiorach. comm -12 <(printf '%s\n' "$db_ts" | sort) <(printf '%s\n' "$media_ts" | sort) | sort -r fi } +# Pojedyncze zrzuty DB (bez pary media): plain SQL / gzip / custom. +list_db_files() { + find "$HOST_BACKUP_DIR" -maxdepth 1 -type f \ + \( -name '*.sql' -o -name '*.sql.gz' -o -name '*.dump' -o -name '*.custom' \) \ + 2>/dev/null | sort -r +} + describe_ts() { local ts="$1" db_path media_path db_path="$HOST_BACKUP_DIR/db-backup-${ts}.tar.gz" @@ -192,72 +328,128 @@ describe_ts() { printf '%s db=%-10s media=%-10s' "$ts" "$db_size" "$media_size" } -pick_timestamp_interactive() { - local timestamps - timestamps="$(list_timestamps)" - if [ -z "$timestamps" ]; then - echo "BLAD: brak kompletnych par backupow w $HOST_BACKUP_DIR." >&2 +# Buduje liste wyboru. Kazda linia: "\t", gdzie TAG to +# "pair:" albo "file:". Etykieta jest czytelna dla czlowieka. +build_pick_lines() { + local ts f + while IFS= read -r ts; do + [ -z "$ts" ] && continue + printf 'pair:%s\t[para ] %s\n' "$ts" "$(describe_ts "$ts")" + done < <(list_timestamps) + # Pojedyncze pliki DB tylko gdy nie jestesmy w trybie --media-only. + if [ "$MEDIA_ONLY" != 1 ]; then + while IFS= read -r f; do + [ -z "$f" ] && continue + printf 'file:%s\t[plik ] %s (%s)\n' "$f" "$(basename "$f")" "$(fmt_size "$f")" + done < <(list_db_files) + fi +} + +# Interaktywny wybor. Echo na STDOUT: "pair:" albo "file:". +pick_source_interactive() { + local lines + lines="$(build_pick_lines)" + if [ -z "$lines" ]; then + echo "BLAD: brak backupow w $HOST_BACKUP_DIR (par db+media ani plikow DB)." >&2 exit 1 fi if command -v fzf >/dev/null 2>&1; then - local lines selected - lines="$(while IFS= read -r ts; do describe_ts "$ts"; printf '\n'; done <<<"$timestamps")" - selected="$(printf '%s' "$lines" | fzf --height=40% --reverse --prompt='Wybierz backup> ' \ - --header='ENTER=wybierz, ESC=anuluj' || true)" - if [ -z "$selected" ]; then - echo "Anulowane." >&2 - exit 1 - fi - # Pierwsze pole to timestamp (przed pierwsza spacja). - printf '%s' "${selected%% *}" + local selected + selected="$(printf '%s\n' "$lines" \ + | fzf --height=40% --reverse --delimiter='\t' --with-nth=2.. \ + --prompt='Wybierz backup> ' --header='ENTER=wybierz, ESC=anuluj' \ + || true)" + [ -z "$selected" ] && { echo "Anulowane." >&2; exit 1; } + printf '%s' "${selected%%$'\t'*}" else - echo "(fzf nie znalezione - prosty wybor)" + echo "(fzf nie znalezione - prosty wybor)" >&2 local i=1 - local -a ts_arr=() - while IFS= read -r ts; do - [ -z "$ts" ] && continue - ts_arr+=("$ts") - printf ' [%d] %s\n' "$i" "$(describe_ts "$ts")" >&2 + local -a tags=() + local tag label + while IFS=$'\t' read -r tag label; do + [ -z "$tag" ] && continue + tags+=("$tag") + printf ' [%d] %s\n' "$i" "$label" >&2 i=$((i + 1)) - done <<<"$timestamps" + done <<<"$lines" local choice - read -r -p "Numer backupu (1-${#ts_arr[@]}): " choice - if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt "${#ts_arr[@]}" ]; then + read -r -p "Numer (1-${#tags[@]}): " choice + if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt "${#tags[@]}" ]; then echo "BLAD: nieprawidlowy wybor." >&2 exit 1 fi - printf '%s' "${ts_arr[$((choice - 1))]}" + printf '%s' "${tags[$((choice - 1))]}" fi } -if [ -n "$TIMESTAMP_ARG" ]; then - TS="$TIMESTAMP_ARG" +# ---- Rozwiazanie zrodla restore ----------------------------------------- +# SINGLE_DB=1 => restore pojedynczego pliku DB (bez media). DB_SRC => sciezka. +# Tryb par => TS ustawione, DB_TAR_PATH/MEDIA_TAR_PATH wyliczane nizej. +SINGLE_DB=0 +DB_SRC="" +TS="" + +if [ -n "$DB_FILE" ]; then + SINGLE_DB=1 + DB_SRC="$DB_FILE" elif [ "$PICK" = 1 ]; then - TS="$(pick_timestamp_interactive)" + SEL="$(pick_source_interactive)" + case "$SEL" in + file:*) SINGLE_DB=1; DB_SRC="${SEL#file:}" ;; + pair:*) TS="${SEL#pair:}" ;; + *) echo "BLAD: nieoczekiwany wybor: $SEL" >&2; exit 1 ;; + esac +elif [ -n "$TIMESTAMP_ARG" ]; then + TS="$TIMESTAMP_ARG" else TS="$(list_timestamps | head -1)" if [ -z "$TS" ]; then echo "BLAD: brak kompletnych par backupow w $HOST_BACKUP_DIR." >&2 - echo " Uruchom backup recznie ('make backup') lub uzyj --db-only/--media-only." >&2 + echo " Uruchom 'make backup', podaj --db-file=PATH albo uzyj --pick." >&2 exit 1 fi fi -DB_TAR="db-backup-${TS}.tar.gz" -MEDIA_TAR="media-backup-${TS}.tar.gz" -DB_TAR_PATH="$HOST_BACKUP_DIR/$DB_TAR" -MEDIA_TAR_PATH="$HOST_BACKUP_DIR/$MEDIA_TAR" -DUMP_DIRNAME="db-backup-${TS}" +# ---- Walidacja + wyliczenie sciezek ------------------------------------- +if [ "$SINGLE_DB" = 1 ]; then + if [ ! -f "$DB_SRC" ]; then + echo "BLAD: nie znaleziono pliku zrzutu DB: $DB_SRC" >&2 + exit 1 + fi + DB_SRC="$(cd "$(dirname "$DB_SRC")" && pwd)/$(basename "$DB_SRC")" + # Detekcja / override formatu. + if [ "$DB_FORMAT" = auto ]; then + FMT="$(detect_db_format "$DB_SRC")" + else + FMT="$DB_FORMAT" + if { [ "$FMT" = custom ] || [ "$FMT" = plain ]; } \ + && gzip -t "$DB_SRC" 2>/dev/null; then + FMT="${FMT}-gz" + fi + fi +else + DB_TAR="db-backup-${TS}.tar.gz" + MEDIA_TAR="media-backup-${TS}.tar.gz" + DB_TAR_PATH="$HOST_BACKUP_DIR/$DB_TAR" + MEDIA_TAR_PATH="$HOST_BACKUP_DIR/$MEDIA_TAR" -# Walidacja istnienia plikow -if [ "$MEDIA_ONLY" != 1 ] && [ ! -f "$DB_TAR_PATH" ]; then - echo "BLAD: nie znaleziono $DB_TAR_PATH." >&2 - exit 1 -fi -if [ "$DB_ONLY" != 1 ] && [ ! -f "$MEDIA_TAR_PATH" ]; then - echo "BLAD: nie znaleziono $MEDIA_TAR_PATH." >&2 - exit 1 + if [ "$MEDIA_ONLY" != 1 ] && [ ! -f "$DB_TAR_PATH" ]; then + echo "BLAD: nie znaleziono $DB_TAR_PATH." >&2 + exit 1 + fi + if [ "$DB_ONLY" != 1 ] && [ ! -f "$MEDIA_TAR_PATH" ]; then + echo "BLAD: nie znaleziono $MEDIA_TAR_PATH." >&2 + exit 1 + fi + DB_SRC="$DB_TAR_PATH" + if [ "$MEDIA_ONLY" != 1 ]; then + if [ "$DB_FORMAT" = auto ]; then + FMT="$(detect_db_format "$DB_SRC")" + else + FMT="$DB_FORMAT" + fi + fi fi # ---- Podsumowanie + potwierdzenie --------------------------------------- @@ -265,22 +457,33 @@ echo echo "==========================================================" echo " BPP RESTORE" echo "==========================================================" -echo " Timestamp: $TS" -echo " Katalog backupow: $HOST_BACKUP_DIR" -if [ "$MEDIA_ONLY" != 1 ]; then - echo " DB tarball: $DB_TAR ($(fmt_size "$DB_TAR_PATH"))" -fi -if [ "$DB_ONLY" != 1 ]; then - echo " Media tarball: $MEDIA_TAR ($(fmt_size "$MEDIA_TAR_PATH"))" +if [ "$SINGLE_DB" = 1 ]; then + echo " Tryb: pojedynczy plik DB (media nietkniete)" + echo " DB source: $(basename "$DB_SRC") ($(fmt_size "$DB_SRC"))" + echo " Format DB: $FMT" +else + echo " Tryb: para backupow (timestamp)" + echo " Timestamp: $TS" + if [ "$MEDIA_ONLY" != 1 ]; then + echo " DB tarball: $DB_TAR ($(fmt_size "$DB_TAR_PATH")) format=$FMT" + fi + if [ "$DB_ONLY" != 1 ]; then + echo " Media tarball: $MEDIA_TAR ($(fmt_size "$MEDIA_TAR_PATH"))" + fi fi +echo " Katalog backupow: $HOST_BACKUP_DIR" echo " Project: $COMPOSE_PROJECT_NAME" echo " DB target: $DJANGO_BPP_DB_USER@$DJANGO_BPP_DB_HOST:$DJANGO_BPP_DB_PORT/$DJANGO_BPP_DB_NAME" echo " Safety backup: $([ "$SAFETY_BACKUP" = 1 ] && echo TAK || echo NIE)" echo "==========================================================" echo echo "UWAGA: ta operacja:" -[ "$MEDIA_ONLY" != 1 ] && echo " - DROPNIE i odtworzy baze $DJANGO_BPP_DB_NAME" -[ "$DB_ONLY" != 1 ] && echo " - rozpakuje archiwum mediow na volume ${COMPOSE_PROJECT_NAME}_media" +if [ "$MEDIA_ONLY" != 1 ]; then + echo " - DROPNIE i odtworzy baze $DJANGO_BPP_DB_NAME" +fi +if [ "$SINGLE_DB" != 1 ] && [ "$DB_ONLY" != 1 ]; then + echo " - rozpakuje archiwum mediow na volume ${COMPOSE_PROJECT_NAME}_media" +fi echo " - zatrzyma appserver, workery, beat, denorm-queue na czas operacji" echo @@ -293,10 +496,10 @@ fi if [ "$SAFETY_BACKUP" = 1 ]; then echo echo "=== Safety backup biezacego stanu (przed restorem) ===" - if [ "$MEDIA_ONLY" = 1 ]; then - run make -C "$REPO_DIR" media-backup - elif [ "$DB_ONLY" = 1 ]; then + if [ "$SINGLE_DB" = 1 ] || [ "$DB_ONLY" = 1 ]; then run make -C "$REPO_DIR" db-backup + elif [ "$MEDIA_ONLY" = 1 ]; then + run make -C "$REPO_DIR" media-backup else run make -C "$REPO_DIR" backup fi @@ -305,58 +508,18 @@ fi # ---- Stop dependent services -------------------------------------------- echo echo "=== Stop dependent services ===" -run docker compose -f "$REPO_DIR/docker-compose.yml" stop \ - appserver workerserver denorm-queue celerybeat flower || true +run dc stop appserver workerserver denorm-queue celerybeat flower || true # ---- DB restore --------------------------------------------------------- if [ "$MEDIA_ONLY" != 1 ]; then - echo - echo "=== DB restore z $DB_TAR ===" - - # Tarball jest w $HOST_BACKUP_DIR czyli /backup wewnatrz dbserver (bind-mount). - # Rozpakuj wewnatrz kontenera, restore, posprzataj rozpakowany katalog. - run docker compose -f "$REPO_DIR/docker-compose.yml" exec -T dbserver \ - tar xzf "/backup/$DB_TAR" -C /backup - - # Drop + create. Connection do template1 zeby moc zdropnac biezaca baze. - echo "+ dropdb --force $DJANGO_BPP_DB_NAME" - docker compose -f "$REPO_DIR/docker-compose.yml" exec -T \ - -e "PGPASSWORD=$DJANGO_BPP_DB_PASSWORD" dbserver \ - dropdb --force \ - -h "$DJANGO_BPP_DB_HOST" -p "$DJANGO_BPP_DB_PORT" \ - -U "$DJANGO_BPP_DB_USER" "$DJANGO_BPP_DB_NAME" - - echo "+ createdb $DJANGO_BPP_DB_NAME" - docker compose -f "$REPO_DIR/docker-compose.yml" exec -T \ - -e "PGPASSWORD=$DJANGO_BPP_DB_PASSWORD" dbserver \ - createdb \ - -h "$DJANGO_BPP_DB_HOST" -p "$DJANGO_BPP_DB_PORT" \ - -U "$DJANGO_BPP_DB_USER" "$DJANGO_BPP_DB_NAME" - - echo "+ pg_restore -Fd -j $PARALLEL_JOBS" - docker compose -f "$REPO_DIR/docker-compose.yml" exec -T \ - -e "PGPASSWORD=$DJANGO_BPP_DB_PASSWORD" dbserver \ - pg_restore \ - -Fd -j "$PARALLEL_JOBS" \ - -h "$DJANGO_BPP_DB_HOST" -p "$DJANGO_BPP_DB_PORT" \ - -U "$DJANGO_BPP_DB_USER" -d "$DJANGO_BPP_DB_NAME" \ - --no-owner --no-privileges \ - "/backup/$DUMP_DIRNAME" - - # Sprzatamy rozpakowany katalog (tarball ZOSTAJE jako disaster recovery). - run docker compose -f "$REPO_DIR/docker-compose.yml" exec -T dbserver \ - rm -rf "/backup/$DUMP_DIRNAME" - echo "DB restore: OK" + restore_db "$DB_SRC" "$FMT" fi # ---- Media restore ------------------------------------------------------ -if [ "$DB_ONLY" != 1 ]; then +if [ "$SINGLE_DB" != 1 ] && [ "$DB_ONLY" != 1 ]; then echo echo "=== Media restore z $MEDIA_TAR ===" - # Volume montujemy rw, katalog z tarballem ro, rozpakowujemy. Identyczna - # logika jak `media-backup` w mk/database.mk, tylko w druga strone. - # Zalozenie: volume jest pusty albo zgadzasz sie na merge (overlay). - # Uzytkownik potwierdzil w brainstormie ze use-case to pusty volume. + # Volume rw, katalog z tarballem ro, rozpakowujemy (overlay na volume). run docker run --rm \ -v "${COMPOSE_PROJECT_NAME}_media:/dst" \ -v "$HOST_BACKUP_DIR:/backup:ro" \ @@ -374,9 +537,13 @@ echo echo "==========================================================" echo " RESTORE ZAKONCZONY POMYSLNIE" echo "==========================================================" -echo " Timestamp restored: $TS" -[ "$MEDIA_ONLY" != 1 ] && echo " DB: $DB_TAR" -[ "$DB_ONLY" != 1 ] && echo " Media: $MEDIA_TAR" +if [ "$SINGLE_DB" = 1 ]; then + echo " DB source: $(basename "$DB_SRC") (format: $FMT)" +else + echo " Timestamp restored: $TS" + [ "$MEDIA_ONLY" != 1 ] && echo " DB: $DB_TAR (format: $FMT)" + [ "$DB_ONLY" != 1 ] && echo " Media: $MEDIA_TAR" +fi echo echo "Sprawdz stan stacka: make health / make logs-appserver" -echo "Tarballe pozostaja w $HOST_BACKUP_DIR (nie zostaly usuniete)." +echo "Pliki zrodlowe pozostaja w $HOST_BACKUP_DIR (nie zostaly usuniete)." From e372d60e4804b1724ca3f24c57be0eea82bc7202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sat, 13 Jun 2026 22:36:54 +0200 Subject: [PATCH 08/15] fix(collation-migrate): wykrywaj plpython przez DDL, nie nazwy migracji MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sanity-check plpython w kroku 2 dawal falszywy alarm: `grep plpython3u| LANGUAGE plpython` lapal slowo "plpython" w NAZWACH migracji w danych django_migrations (0440_port_plpython_to_plpgsql, 0442_drop_plpython3u) — czyli ostrzegal nawet na zrzutach JUZ po usunieciu plpythona. Teraz szukamy REALNEGO uzycia, ktore faktycznie wywroci load na stocku: ^CREATE EXTENSION ... plpython | LANGUAGE plpython Nazwy migracji (bez "CREATE EXTENSION"/"LANGUAGE " przed plpython) nie pasuja. Zweryfikowane: post-0442 (tylko nazwy migracji) -> brak ostrzezenia; pre-0442 (CREATE EXTENSION plpython3u + LANGUAGE plpython3u) -> ostrzega. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/pg-collation-migrate-2-fix.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/pg-collation-migrate-2-fix.sh b/scripts/pg-collation-migrate-2-fix.sh index ccec361..e1c0cdf 100755 --- a/scripts/pg-collation-migrate-2-fix.sh +++ b/scripts/pg-collation-migrate-2-fix.sh @@ -86,8 +86,12 @@ fi # Sanity: zrzut sprzed migracji bpp 0442 ma jeszcze plpython3u, ktorego stock # postgres nie ma -> load padnie. Ostrzegamy (nie blokujemy). -if grep -qiE 'plpython3u|LANGUAGE plpython' "$OUT_SQL"; then - echo "!! OSTRZEZENIE: zrzut zawiera plpython3u (sprzed migracji bpp 0442)." >&2 +# WAZNE: szukamy REALNEGO uzycia (CREATE EXTENSION ... plpython / LANGUAGE +# plpython w funkcjach), a NIE samego slowa "plpython" — bo to lapie nazwy +# migracji w danych django_migrations (np. 0440_port_plpython_to_plpgsql, +# 0442_drop_plpython3u), dajac falszywy alarm na zrzutach JUZ po migracji. +if grep -qiE '^CREATE[[:space:]]+EXTENSION[[:space:]].*plpython|LANGUAGE[[:space:]]+plpython' "$OUT_SQL"; then + echo "!! OSTRZEZENIE: zrzut zawiera REALNE uzycie plpython (CREATE EXTENSION / LANGUAGE)." >&2 echo " Stock postgres go nie ma -> krok 3 padnie. Zrob NOWY zrzut po" >&2 echo " wdrozeniu wersji aplikacji z migracja 0442 (DROP EXTENSION plpython3u)." >&2 fi From 03a3b78c76ce6bccdf5cd6a32c2afdcb8aa3bc75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Tue, 16 Jun 2026 20:09:54 +0200 Subject: [PATCH 09/15] Fix hstore --- scripts/pg-collation-migrate-3-load.sh | 37 +++++++++++++++++++++----- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/scripts/pg-collation-migrate-3-load.sh b/scripts/pg-collation-migrate-3-load.sh index e4985a3..4659e89 100755 --- a/scripts/pg-collation-migrate-3-load.sh +++ b/scripts/pg-collation-migrate-3-load.sh @@ -104,14 +104,39 @@ if [ "${PROV:-}" != "i" ]; then echo " POSTGRES_INITDB_ARGS=--locale-provider=icu --icu-locale=pl-PL ?" >&2 fi -echo ">> Laduje ${SQL_FILE} (psql, ON_ERROR_STOP=1)..." >&2 -# Plain SQL z pliku -> psql w kontenerze (dc exec -T forwarduje stdin). Gdy -# jest `pv` i stderr to terminal, pokazujemy pasek postepu (pv sam zna rozmiar -# pliku). pipefail (set -o) wylapie blad psql mimo pv po lewej stronie potoku. +# Fix hstore-w-WHEN na PG18: pg_dump utwardza naglowek przez +# `SELECT pg_catalog.set_config('search_path', '', false);` (ochrona po +# CVE-2018-1058 — PUSTY search_path na cala sesje restore). Przy odtwarzaniu +# triggerow STAREGO denorma klauzula WHEN porownuje kolumne `legacy_data` +# (typ hstore) operatorem `IS DISTINCT FROM`, ktory rozwija sie do `hstore = +# hstore`. Ten operator zyje w schemacie `public` (CREATE EXTENSION hstore +# WITH SCHEMA public), a klauzula WHEN jest parsowana JUZ przy CREATE TRIGGER +# (niezaleznie od check_function_bodies). Z pustym search_path operator jest +# niewidoczny -> "operator does not exist: public.hstore = public.hstore". +# PG16 to przepuszczal, PG18 nie. Przywracamy `public` do search_path na czas +# restore (zachowanie sprzed CVE; bezpieczne, bo pg_dump kwalifikuje obiekty +# schematem). Robimy to filtrem strumieniowym, BEZ modyfikacji zapisanego +# pliku -nocollation.sql. +SEARCH_PATH_FIX=\ +"s/set_config('search_path', '', false)/set_config('search_path', 'public', false)/" +if ! head -n 100 "$SQL_FILE" | grep -q "set_config('search_path', '', false)"; then + echo "!! UWAGA: nie znalazlem w naglowku zrzutu linii" >&2 + echo " set_config('search_path', '', false) — fix search_path bedzie no-op." >&2 + echo " Jesli load padnie na 'operator does not exist: public.hstore = public.hstore'," >&2 + echo " format pg_dump sie zmienil — popraw wzorzec SEARCH_PATH_FIX w tym skrypcie." >&2 +fi + +echo ">> Laduje ${SQL_FILE} (psql, ON_ERROR_STOP=1, search_path->public)..." >&2 +# Plain SQL z pliku -> sed (fix search_path) -> psql w kontenerze (dc exec -T +# forwarduje stdin). Gdy jest `pv` i stderr to terminal, pokazujemy pasek +# postepu (pv sam zna rozmiar pliku, bo czyta go PRZED sedem). pipefail +# (set -o) wylapie blad psql mimo pv/sed po lewej stronie potoku. if [ -t 2 ] && command -v pv >/dev/null 2>&1; then - pv "$SQL_FILE" | "${PSQL[@]}" -v ON_ERROR_STOP=1 -d "${DJANGO_BPP_DB_NAME}" + pv "$SQL_FILE" | sed "$SEARCH_PATH_FIX" \ + | "${PSQL[@]}" -v ON_ERROR_STOP=1 -d "${DJANGO_BPP_DB_NAME}" else - "${PSQL[@]}" -v ON_ERROR_STOP=1 -d "${DJANGO_BPP_DB_NAME}" < "$SQL_FILE" + sed "$SEARCH_PATH_FIX" "$SQL_FILE" \ + | "${PSQL[@]}" -v ON_ERROR_STOP=1 -d "${DJANGO_BPP_DB_NAME}" fi echo ">> Weryfikacja po loadzie:" >&2 From d44333f74273cdf3fdb6df41f6ca21d3454f764c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Wed, 17 Jun 2026 21:10:25 +0200 Subject: [PATCH 10/15] =?UTF-8?q?docs(collation-migrate):=20popraw=20instr?= =?UTF-8?q?ukcje=20po=20loadzie=20=E2=80=94=20make=20up=20zamiast=20make?= =?UTF-8?q?=20migrate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Appserver przy starcie sam przepuszcza migracje; 'make migrate' wymaga juz dzialajacego appservera (robi docker compose exec appserver ...). Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/pg-collation-migrate-3-load.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/pg-collation-migrate-3-load.sh b/scripts/pg-collation-migrate-3-load.sh index 4659e89..20211ef 100755 --- a/scripts/pg-collation-migrate-3-load.sh +++ b/scripts/pg-collation-migrate-3-load.sh @@ -147,4 +147,7 @@ echo ">> Weryfikacja po loadzie:" >&2 "${PSQL[@]}" -d "${DJANGO_BPP_DB_NAME}" -tAc \ "SELECT 'kolacje pl_PL w public pozostale: '||count(*) FROM pg_collation c JOIN pg_namespace n ON n.oid=c.collnamespace WHERE lower(c.collname)='pl_pl' AND n.nspname='public';" >&2 echo ">> Gotowe. Baza '${DJANGO_BPP_DB_NAME}' zaladowana na ${IMG}." >&2 -echo " Teraz: zmigruj aplikacje ('make migrate') i wstan stack ('make up')." >&2 +echo " Teraz: wstan stack ('make up') — appserver przy starcie sam" >&2 +echo " przepusci migracje. Ewentualnie potem 'make migrate' na zywym" >&2 +echo " stacku ('make migrate' robi 'docker compose exec appserver ...'," >&2 +echo " wiec wymaga juz dzialajacego appservera)." >&2 From 658cfeae11c2b5b260e0ebf3205d7f761e7263cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Wed, 17 Jun 2026 21:12:04 +0200 Subject: [PATCH 11/15] fix(collation-migrate): krok 1 pyta o stop tylko gdy realnie dzialaja pisarze MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ostrzezenie/prompt 'aplikacja NIE jest zatrzymywana' bylo bezwarunkowe — mylace, gdy stack stoi a recznie wstal tylko dbserver. Teraz skrypt sprawdza, ktore z serwisow-pisarzy (appserver, workerserver, celerybeat, denorm-queue) faktycznie chodza: pyta tylko o dzialajace, a gdy zaden nie dziala — leci bez promptu (zrzut i tak spojny). Pisarzami sa tez celery/denorm-queue, nie tylko appserver. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/pg-collation-migrate-1-dump.sh | 28 ++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/scripts/pg-collation-migrate-1-dump.sh b/scripts/pg-collation-migrate-1-dump.sh index f03aaf9..074f6b1 100755 --- a/scripts/pg-collation-migrate-1-dump.sh +++ b/scripts/pg-collation-migrate-1-dump.sh @@ -73,13 +73,33 @@ if ! dc ps --status=running --services 2>/dev/null | grep -qx dbserver; then exit 1 fi +# Serwisy ktore moga PISAC do bazy (psuja spojnosc rownoleglego zrzutu). +# Nie tylko appserver: takze celery (workery/beat) i denorm-queue. To te same, +# ktore zatrzymuje --stop-app. +WRITER_SVCS="appserver workerserver celerybeat denorm-queue" if [ "$STOP_APP" = 1 ]; then echo ">> Zatrzymuje aplikacje (appserver + workery + beat + denorm-queue)..." >&2 - run dc stop appserver workerserver celerybeat denorm-queue + run dc stop $WRITER_SVCS else - echo ">> UWAGA: aplikacja NIE jest zatrzymywana (--stop-app pominiete)." >&2 - echo " Zrzut spojny tylko jesli nie ma rownoleglych zapisow do bazy." >&2 - confirm "Kontynuowac zrzut bez zatrzymania aplikacji?" || { echo "Przerwano."; exit 1; } + # Ostrzegaj/pytaj TYLKO o realnie dzialajace serwisy-pisarzy. Gdy zaden nie + # chodzi (np. caly stack stoi, a recznie wstal tylko dbserver), zrzut jest + # spojny i nie ma o co pytac -> bez promptu. + RUNNING_SVCS="$(dc ps --status=running --services 2>/dev/null || true)" + RUNNING_WRITERS="" + for svc in $WRITER_SVCS; do + if printf '%s\n' "$RUNNING_SVCS" | grep -qx "$svc"; then + RUNNING_WRITERS="${RUNNING_WRITERS:+$RUNNING_WRITERS }$svc" + fi + done + if [ -n "$RUNNING_WRITERS" ]; then + echo ">> UWAGA: dzialaja serwisy mogace pisac do bazy: ${RUNNING_WRITERS}" >&2 + echo " (--stop-app pominiete). Zrzut spojny tylko bez rownoleglych zapisow." >&2 + confirm "Kontynuowac zrzut mimo dzialajacych: ${RUNNING_WRITERS}?" \ + || { echo "Przerwano."; exit 1; } + else + echo ">> Zaden serwis-pisarz (appserver/workery/beat/denorm-queue) nie dziala" >&2 + echo " -> zrzut spojny, kontynuuje bez zatrzymywania." >&2 + fi fi # Pasek postepu: jesli na hoscie jest `pv` i stderr to terminal, wpinamy go From 945026f30fda5ebabe9397f498265f15b776f241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Wed, 17 Jun 2026 21:25:40 +0200 Subject: [PATCH 12/15] fix(upgrade-postgres): make up przed make migrate w kroku 10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit make migrate robi 'docker compose exec appserver ...', wiec wymaga juz dzialajacego appservera. Po kroku 2 appserver jest zatrzymany, a krok 7 wstawia tylko dbserver — wiec dotychczasowa kolejnosc 'make migrate' -> 'make up' wywalala sie z "service appserver is not running". make up jest teraz pierwsze (appserver przy starcie sam przepuszcza migracje), make migrate po nim to bezpieczne, jawne powtorzenie. Bug przezyl bo test-upgrade-postgres.sh jawnie pomija krok 10. Zaktualizowano tez opisy kroku 10 (naglowek skryptu, komunikaty, komentarz testu, docs/konfiguracja/postgresql.md). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/konfiguracja/postgresql.md | 4 +++- scripts/test-upgrade-postgres.sh | 2 +- scripts/upgrade-postgres.sh | 15 ++++++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/konfiguracja/postgresql.md b/docs/konfiguracja/postgresql.md index c3c3552..a024970 100644 --- a/docs/konfiguracja/postgresql.md +++ b/docs/konfiguracja/postgresql.md @@ -66,7 +66,9 @@ Skrypt (`scripts/upgrade-postgres.sh`) interaktywnie wykonuje kroki: 6. Bump `DJANGO_BPP_POSTGRESQL_VERSION` (+ `_MAJOR`) w `.env` 7. `docker compose pull dbserver` + `up -d dbserver` → initdb na nowym majorze 8. `pg_restore -Fd -j N` z tarballa -9. `make migrate` + `make up` + smoke-test logów appservera +9. `make up` + `make migrate` + smoke-test logów appservera (kolejność istotna: + `make migrate` robi `docker compose exec appserver …`, więc wymaga już + działającego appservera — `make up` musi być pierwsze) ### Wymagania diff --git a/scripts/test-upgrade-postgres.sh b/scripts/test-upgrade-postgres.sh index 90cfeea..35d53ae 100755 --- a/scripts/test-upgrade-postgres.sh +++ b/scripts/test-upgrade-postgres.sh @@ -11,7 +11,7 @@ # - tymczasowo podmienione repo .env (make db-backup czyta stamtad # BPP_CONFIGS_DIR; cleanup trap przywraca oryginal) # -# Kroki 2 (stop serwisow) i 10 (make migrate + make up) sa pomijane bo wymagaja +# Kroki 2 (stop serwisow) i 10 (make up + make migrate) sa pomijane bo wymagaja # pelnego stacka. Pozostale 1, 3-9 lecza przez prawdziwy skrypt. # # Uruchomienie: `make test-upgrade-postgres` lub diff --git a/scripts/upgrade-postgres.sh b/scripts/upgrade-postgres.sh index c3ab2e4..688e249 100755 --- a/scripts/upgrade-postgres.sh +++ b/scripts/upgrade-postgres.sh @@ -9,7 +9,7 @@ # 4. docker compose pull dbserver -> nowy obraz z nowym majorem # 5. docker compose up -d dbserver -> initdb na nowej wersji w pustym volume # 6. pg_restore -Fd -j N z tarballa -# 7. make migrate, make up, smoke test +# 7. make up, make migrate, smoke test # # Wybor dump & restore zamiast pg_upgrade jest swiadomy: kazdy obraz postgres: # ma tylko jeden major Postgresa, wiec pg_upgrade in-place wymagalby dedykowanego @@ -142,7 +142,7 @@ Kroki: 7. Bump DJANGO_BPP_POSTGRESQL_VERSION(+_MAJOR) w .env 8. Start nowego dbservera (initdb na nowym majorze) 9. pg_restore z tarballa - 10. make migrate + make up + smoke test + 10. make up + make migrate + smoke test Uwaga: niektore kroki nie sa w pelni idempotentne przy wznowieniu. Np. krok 5 wywali sie jesli BACKUP_VOLUME juz istnieje (wtedy usun go recznie @@ -489,7 +489,7 @@ Procedura wykona: 8. Start nowego dbserver -> initdb na nowym majorze (w razie failu - prompt o auto-rollback z backup volume) 9. pg_restore z tarballa - 10. make migrate, make up, smoke test + 10. make up, make migrate, smoke test Stary volume bedzie zachowany pod nowa nazwa az do recznego usuniecia. Tarball z pg_dump tez zostaje w \$DJANGO_BPP_HOST_BACKUP_DIR. @@ -928,9 +928,14 @@ CRITICAL_STAGE_REACHED=0 if [ "$FROM_STEP" -le 10 ] && ! step_is_skipped 10; then CURRENT_STEP=10 echo - echo "=== [10/10] make migrate + make up ===" - run make migrate + echo "=== [10/10] make up + make migrate ===" + # make up MUSI byc pierwsze: appserver przy starcie sam przepuszcza + # migracje, a 'make migrate' robi 'docker compose exec appserver ...', + # wiec wymaga juz dzialajacego appservera (po kroku 2 byl zatrzymany, + # krok 7 wstawia tylko dbserver). 'make migrate' po 'make up' jest + # bezpiecznym, jawnym powtorzeniem (stop denorm-workerow na czas migracji). run make up + run make migrate echo echo "Smoke test - logi appserver (ostatnie 30 lini):" From 8f6d0bbde2f8750c688971faea70e5322521354c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Wed, 17 Jun 2026 21:27:52 +0200 Subject: [PATCH 13/15] feat(collation-migrate): samokalibrujacy pasek pv przy zrzucie (krok 1) pg_database_size to rozmiar FIZYCZNY (z indeksami, TOAST skompresowany). Plain-SQL dump nie zrzuca danych indeksow -> realnie bywa ~40% pg_database_size, przez co goly 'pv -s pg_database_size' konczyl pasek na ~40%. Zamiast zgadywac funkcja katalogu (pg_table_size tez nie odda kodowania COPY ani indeksow), kalibrujemy: po zrzucie zapisujemy ratio = rozmiar_zrzutu / pg_database_size do $HOST_BACKUP_DIR/.pg-dump-size-ratio, a nastepny run mnozy nim estymate. Ratio walidowane do zakresu 0.1..20. Kierunkowo-agnostyczne (dziala gdy dump < lub > bazy). awk wymusza LC_ALL=C, bo pl_PL formatuje ulamek przecinkiem (0,400), co psuloby zapis i walidacje ratio. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/pg-collation-migrate-1-dump.sh | 48 +++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/scripts/pg-collation-migrate-1-dump.sh b/scripts/pg-collation-migrate-1-dump.sh index 074f6b1..f533efe 100755 --- a/scripts/pg-collation-migrate-1-dump.sh +++ b/scripts/pg-collation-migrate-1-dump.sh @@ -103,11 +103,21 @@ else fi # Pasek postepu: jesli na hoscie jest `pv` i stderr to terminal, wpinamy go -# miedzy pg_dump a plik. Jako estymate (-s) bierzemy fizyczny rozmiar bazy -# (pg_database_size) — to tylko przyblizenie (dump nie ma indeksow ani bloatu, -# wiec zwykle konczy przed 100%), ale daje procent + ETA. Gdy pv nie ma, -# identity-pipe przez `cat`. Nieudane zapytanie o rozmiar -> pv bez -s. +# miedzy pg_dump a plik. Estymate (-s) liczymy z pg_database_size, ale UWAGA: +# to rozmiar FIZYCZNY na dysku. Plain-SQL dump nie zrzuca danych indeksow, za +# to ROZPAKOWUJE TOAST i koduje wszystko tekstem (COPY) — dla bpp (duze pola +# tekstowe, tsvector search_index, hstore legacy_data) zrzut bywa ~2x wiekszy +# niz pg_database_size, wiec goly -s zanizal pasek do ~50%. Zadna funkcja +# katalogu nie zna logicznego rozmiaru, wiec KALIBRUJEMY: trzymamy w pliku +# stosunek (rozmiar_zrzutu / pg_database_size) z POPRZEDNIEGO uruchomienia i +# mnozymy nim biezacy pg_database_size. Pierwszy run: ratio=1.0 (jak dotad); +# po zrzucie zapisujemy zmierzony ratio (patrz nizej). Gdy pv nie ma — +# identity-pipe przez `cat`; nieudane zapytanie o rozmiar -> pv bez -s. +RATIO_FILE="${HOST_BACKUP_DIR}/.pg-dump-size-ratio" +file_size() { stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null; } + PROGRESS=(cat) +DB_BYTES=0 if [ -t 2 ] && command -v pv >/dev/null 2>&1; then DB_BYTES="$(dc exec -T -e "PGPASSWORD=${DJANGO_BPP_DB_PASSWORD}" dbserver psql \ -h "${DJANGO_BPP_DB_HOST}" -p "${DJANGO_BPP_DB_PORT}" \ @@ -116,7 +126,19 @@ if [ -t 2 ] && command -v pv >/dev/null 2>&1; then | tr -d '[:space:]')" case "$DB_BYTES" in ''|*[!0-9]*) DB_BYTES=0 ;; esac if [ "$DB_BYTES" -gt 0 ]; then - PROGRESS=(pv -s "$DB_BYTES") + RATIO=1.0 + if [ -r "$RATIO_FILE" ]; then + R="$(tr -d '[:space:]' < "$RATIO_FILE")" + # akceptuj tylko dodatnia liczbe w rozsadnym zakresie 0.1..20 + # (chroni przed uszkodzonym plikiem dajacym absurdalny -s). + if printf '%s' "$R" | grep -qE '^[0-9]+(\.[0-9]+)?$' \ + && LC_ALL=C awk "BEGIN{exit !($R>=0.1 && $R<=20)}"; then + RATIO="$R" + fi + fi + EST="$(LC_ALL=C awk "BEGIN{printf \"%d\", $DB_BYTES * $RATIO}")" + echo ">> Estymata pv: ${EST} B (pg_database_size ${DB_BYTES} B x ratio ${RATIO})" >&2 + PROGRESS=(pv -s "$EST") else PROGRESS=(pv) fi @@ -145,6 +167,22 @@ if ! tail -n 5 "$PARTIAL" | grep -q 'PostgreSQL database dump complete'; then fi mv "$PARTIAL" "$OUT_PATH" +# Kalibracja na przyszlosc: zapisz stosunek realnego rozmiaru zrzutu do +# pg_database_size. Nastepny run uzyje go jako mnoznika estymaty pv (patrz +# wyzej). Tylko gdy znalismy DB_BYTES (pv aktywne, zapytanie sie powiodlo). +if [ "${DB_BYTES:-0}" -gt 0 ]; then + ACTUAL="$(file_size "$OUT_PATH")" + case "$ACTUAL" in ''|*[!0-9]*) ACTUAL=0 ;; esac + if [ "$ACTUAL" -gt 0 ]; then + # LC_ALL=C: separator dziesietny MUSI byc kropka (pl_PL dalby "0,400", + # co nie przejdzie walidacji ^[0-9]+(\.[0-9]+)?$ przy nastepnym odczycie). + NEW_RATIO="$(LC_ALL=C awk "BEGIN{printf \"%.3f\", $ACTUAL / $DB_BYTES}")" + if printf '%s\n' "$NEW_RATIO" > "$RATIO_FILE" 2>/dev/null; then + echo ">> Ratio kalibracji pv zapisane: ${NEW_RATIO} (-> ${RATIO_FILE})" >&2 + fi + fi +fi + echo ">> Gotowe. Zrzut plain SQL:" >&2 # Jedyne, co idzie na czysty STDOUT — sciezka do .sql (dla kroku 2). printf '%s\n' "$OUT_PATH" From 20ac92d6a83de6d5ba17b71e9302ec294df83956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Thu, 18 Jun 2026 10:45:09 +0200 Subject: [PATCH 14/15] fix(collation-migrate): --recreate-volume nie wywala sie gdy volume nie istnieje MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'docker volume rm' na nieistniejacym wolumenie zwraca blad i przy set -e zatrzymywal caly skrypt — a BRAK wolumenu to dokladnie stan docelowy (swiezy cluster i tak powstanie). Usuwamy tylko gdy 'docker volume inspect' potwierdzi istnienie; prawdziwy blad usuwania (np. volume in use) nadal zatrzyma skrypt. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/pg-collation-migrate-3-load.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/pg-collation-migrate-3-load.sh b/scripts/pg-collation-migrate-3-load.sh index 20211ef..978eaf6 100755 --- a/scripts/pg-collation-migrate-3-load.sh +++ b/scripts/pg-collation-migrate-3-load.sh @@ -62,7 +62,15 @@ if [ "$RECREATE" = 1 ]; then || { echo "Przerwano — nic nie ruszone." >&2; exit 1; } run dc stop appserver workerserver celerybeat denorm-queue dbserver || true run dc rm -f dbserver || true - run docker volume rm "$VOL" + # Wolumen kasujemy tylko jesli istnieje — jego BRAK to dokladnie stan + # docelowy (swiezy cluster i tak powstanie). 'docker volume rm' na + # nieistniejacym wolumenie zwraca blad i przy set -e wywalalby skrypt. + # Prawdziwy blad usuwania (np. wolumen w uzyciu) nadal zatrzyma skrypt. + if docker volume inspect "$VOL" >/dev/null 2>&1; then + run docker volume rm "$VOL" + else + echo ">> Volume '$VOL' nie istnieje — pomijam usuwanie." >&2 + fi echo ">> Stawiam swiezy dbserver (initdb wg obrazu z compose)..." >&2 run dc up -d --wait dbserver fi From f676fba4e3a77e45842438fbe608db0cc3a332f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Thu, 18 Jun 2026 11:02:27 +0200 Subject: [PATCH 15/15] feat(dbserver/backup): reuse obrazu dbservera w backup-runnerze + auto-prune po make up; default PG 18.4 - backup-runner uzywa domyslnie tego SAMEGO obrazu co dbserver (postgres:, Debian) zamiast osobnego -alpine -> wspoldzieli 100% warstw, ~0 MB netto zamiast +~350 MB na dysku. Tryb external nadal alpine przez BPP_BACKUP_PG_IMAGE (ustawiany w init-configs, dosypywany do starych .env przez ensure-config-files). Blok command wykrywa apk vs apt-get, wiec dziala na obu obrazach. - make up (a wiec i make run) sprzata Dockera (docker system prune -af) po udanym starcie (--wait) i PRZED pullem html2docx; wypisuje tylko ile GB zwolniono. Bez --volumes -> nazwane wolumeny danych sa bezpieczne. - Domyslna wersja PostgreSQL dla NOWYCH instalacji: 16.13 -> 18.4 (init-configs prompt + fresh .env). Safety-nety w compose (:-16.13) oraz migracja dla .env bez zmiennej CELOWO zostaja na 16.13 - kompatybilnosc wsteczna, zaden istniejacy klaster nie dostaje cichego upgrade'u majora. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 3 ++- docker-compose.backup.yml | 32 +++++++++++++++++++++------- docs/eksploatacja/backup-i-rclone.md | 16 ++++++++++++-- docs/eksploatacja/komendy.md | 15 +++++++++++-- docs/konfiguracja/postgresql.md | 27 +++++++++++++++++------ mk/deployment.mk | 10 +++++++++ scripts/ensure-config-files.sh | 17 +++++++++++++++ scripts/init-configs.sh | 14 +++++++----- 8 files changed, 109 insertions(+), 25 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2d58995..ed68f2c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,7 +83,7 @@ User uploads land in the `media` volume mounted at `/mediaroot` in every Django ### PostgreSQL version vars -`dbserver` uses the **stock official** `postgres:${DJANGO_BPP_POSTGRESQL_VERSION}` image (Debian, **not** `-alpine` — the entrypoint needs `bash`; `MAJOR.MINOR`, default `16.13`) with the autotune scripts in **`dbserver/`** (`autotune.sh` + `docker-entrypoint-autotune.sh`, copied verbatim from `iplweb/bpp-dbserver`) **bind-mounted** read-only on top. The old `iplweb/bpp_dbserver` image is **discontinued** — autotune was its only delta over stock postgres. These scripts are versioned code delivered by `git pull` — **not** force-synced into `$BPP_CONFIGS_DIR`. CRITICAL contracts: (1) `PGDATA` is pinned to `/var/lib/postgresql/data` (stock `postgres:18+` defaults to a versioned subdir → would ignore the existing volume and re-init blank — never change the mount to `/var/lib/postgresql`); (2) fresh installs init with `POSTGRES_INITDB_ARGS=--locale-provider=icu --icu-locale=pl-PL` (Polish collation; **fresh PGDATA only**, never re-collates existing clusters); (3) stock postgres has no built-in healthcheck, so `dbserver` defines its own `pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"` (appserver/authserver `depend_on: service_healthy`); (4) `dbserver` needs a **service-level** `env_file: ${BPP_CONFIGS_DIR}/.env` — the `include`-level `env_file` is interpolation-only and is NOT injected into the container. `DJANGO_BPP_POSTGRESQL_VERSION_MAJOR` (auto-derived) is used by `backup-runner` (`postgres:-alpine`). Major upgrades require dump/restore — use `make upgrade-postgres`, do **not** edit the var manually. Full procedure (rollback, resume): `docs/konfiguracja/postgresql.md`. +`dbserver` uses the **stock official** `postgres:${DJANGO_BPP_POSTGRESQL_VERSION}` image (Debian, **not** `-alpine` — the entrypoint needs `bash`; `MAJOR.MINOR`; fresh installs default **`18.4`** via `init-configs`, but the Compose safety-net stays `:-16.13` so an ancient `.env`-less PG16 install isn't silently handed a PG18 image) with the autotune scripts in **`dbserver/`** (`autotune.sh` + `docker-entrypoint-autotune.sh`, copied verbatim from `iplweb/bpp-dbserver`) **bind-mounted** read-only on top. The old `iplweb/bpp_dbserver` image is **discontinued** — autotune was its only delta over stock postgres. These scripts are versioned code delivered by `git pull` — **not** force-synced into `$BPP_CONFIGS_DIR`. CRITICAL contracts: (1) `PGDATA` is pinned to `/var/lib/postgresql/data` (stock `postgres:18+` defaults to a versioned subdir → would ignore the existing volume and re-init blank — never change the mount to `/var/lib/postgresql`); (2) fresh installs init with `POSTGRES_INITDB_ARGS=--locale-provider=icu --icu-locale=pl-PL` (Polish collation; **fresh PGDATA only**, never re-collates existing clusters); (3) stock postgres has no built-in healthcheck, so `dbserver` defines its own `pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"` (appserver/authserver `depend_on: service_healthy`); (4) `dbserver` needs a **service-level** `env_file: ${BPP_CONFIGS_DIR}/.env` — the `include`-level `env_file` is interpolation-only and is NOT injected into the container. `DJANGO_BPP_POSTGRESQL_VERSION_MAJOR` (auto-derived) drives the **external-mode sentinel** tag and the upgrade step. `backup-runner` **shares an image** rather than pulling its own: its `image:` is `${BPP_BACKUP_PG_IMAGE:-postgres:${DJANGO_BPP_POSTGRESQL_VERSION:-${DJANGO_BPP_DBSERVER_PG_VERSION:-16.13}}}` — **unset by default → the exact same Debian image as `dbserver`** (100% shared layers, ~0 MB extra on disk; an `-alpine` would share nothing with Debian and cost ~350 MB). Its `command` detects `apk` vs `apt-get` so it works on **both** images. Only **external mode** sets `BPP_BACKUP_PG_IMAGE=postgres:-alpine` (so backup shares with the alpine sentinel, not a stray Debian): written by `init-configs` on fresh installs, self-healed into old `.env` by `ensure-config-files` on `make up` (gated on `BPP_DATABASE_COMPOSE=docker-compose.database.external.yml`). New var → Compose default present, no migration needed. Major upgrades require dump/restore — use `make upgrade-postgres`, do **not** edit the var manually. Full procedure (rollback, resume): `docs/konfiguracja/postgresql.md`. ## Critical Deployment Patterns @@ -149,6 +149,7 @@ Calendar versioning `YYYY.MM.DD[.N]`. `make release` (`scripts/release.sh`): com ## Safety +- `make up` (hence `make run`) ends with `docker system prune -af` **after** the stack is healthy (`--wait`) and **before** the html2docx pull (so the fallback image survives). No `--volumes` → named data volumes are safe; but `-af` removes **all** unused images host-wide (incl. non-BPP). Use `make up-quick` on shared/dev hosts to skip it. Don't "fix" this by adding `--volumes`. - Always `make db-backup` before major changes - Use `make` targets instead of raw `docker compose` (they handle dependencies) - Verify environment-specific config (database markers, backup settings) before destructive operations diff --git a/docker-compose.backup.yml b/docker-compose.backup.yml index 5399bfd..db2e0e7 100644 --- a/docker-compose.backup.yml +++ b/docker-compose.backup.yml @@ -14,14 +14,22 @@ services: # 5. notyfikacja do Rollbara (level=info/error) # # Wszystko w jednym kontenerze, wolany raz dziennie przez Ofelie. - # Obraz postgres:-alpine daje pg_dump w tej samej wersji co - # sentinel/prawdziwy dbserver. rclone, curl, jq, bash, coreutils - # doinstalowane w runtime przez apk add. + # Obraz: domyslnie TEN SAM pelny obraz co dbserver (postgres:, + # Debian) -> backup-runner wspoldzieli z nim 100% warstw, czyli ~0 MB netto + # na dysku (alpine NIE dzieli warstw z Debianem - byly osobne +~350 MB). + # pg_dump trafia dokladnie w wersje serwera. rclone, curl, jq doinstalowane + # w runtime (apt-get na Debianie / apk na alpine - patrz command nizej). + # Tryb external: dbserver to lekki sentinel postgres:-alpine, wiec + # init-configs/ensure-config-files ustawiaja BPP_BACKUP_PG_IMAGE= + # postgres:-alpine, by backup-runner wspoldzielil warstwy z sentinelem + # (a nie sciagal osobnego Debiana). backup-runner: - # Major Postgresa: DJANGO_BPP_POSTGRESQL_VERSION_MAJOR (nowa nazwa) z - # fallbackiem na DJANGO_BPP_POSTGRESQL_DB_VERSION (stara, sprzed rename - # w 2026-04-18). Default 17 jesli .env nie ma zadnej z nich. - image: postgres:${DJANGO_BPP_POSTGRESQL_VERSION_MAJOR:-${DJANGO_BPP_POSTGRESQL_DB_VERSION:-17}}-alpine + # BPP_BACKUP_PG_IMAGE: override obrazu (ustawiany TYLKO w trybie external). + # Nieustawiony -> ten sam tag co dbserver: DJANGO_BPP_POSTGRESQL_VERSION + # (MAJOR.MINOR) z dwuwarstwowym fallbackiem (stara nazwa DBSERVER_PG_VERSION, + # default 16.13) - identyczne wyrazenie jak w docker-compose.database.yml, + # wiec tag rozwiazuje sie do tego samego obrazu (realne wspoldzielenie warstw). + image: ${BPP_BACKUP_PG_IMAGE:-postgres:${DJANGO_BPP_POSTGRESQL_VERSION:-${DJANGO_BPP_DBSERVER_PG_VERSION:-16.13}}} restart: always logging: *default-logging env_file: ${BPP_CONFIGS_DIR}/.env @@ -33,9 +41,17 @@ services: - ${BPP_CONFIGS_DIR}/rclone:/config/rclone:ro - ./scripts:/scripts:ro entrypoint: ["/bin/sh", "-c"] + # Ten sam blok dziala na OBU obrazach: Debian (default, apt-get) i alpine + # (tryb external, apk). Bash i coreutils sa juz w Debianie - doinstalowujemy + # tylko rclone/curl/jq; na alpine dobieramy tez bash i coreutils. command: - | - apk add --no-cache rclone curl jq bash coreutils >/dev/null 2>&1 || exit 1 + if command -v apk >/dev/null 2>&1; then + apk add --no-cache rclone curl jq bash coreutils >/dev/null 2>&1 || exit 1 + else + apt-get update >/dev/null 2>&1 \ + && apt-get install -y --no-install-recommends rclone curl jq >/dev/null 2>&1 || exit 1 + fi exec sleep infinity healthcheck: test: ["CMD-SHELL", "command -v rclone >/dev/null && command -v curl >/dev/null && command -v pg_dump >/dev/null && command -v jq >/dev/null"] diff --git a/docs/eksploatacja/backup-i-rclone.md b/docs/eksploatacja/backup-i-rclone.md index fc982ff..f9baecc 100644 --- a/docs/eksploatacja/backup-i-rclone.md +++ b/docs/eksploatacja/backup-i-rclone.md @@ -13,8 +13,20 @@ make rclone-check # Sprawdzenie spójności kopii zdalnej ## Codzienny backup Codzienny backup uruchamia Ofelia o **02:30** (label `0 30 2 * * *` na `backup-runner`). -`backup-runner` to efemeryczny kontener (`postgres:$DJANGO_BPP_POSTGRESQL_VERSION_MAJOR-alpine`): -robi `pg_dump`, pakuje media (tar), wysyła przez rclone i raportuje do Rollbara. +`backup-runner` to efemeryczny kontener: robi `pg_dump`, pakuje media (tar), wysyła +przez rclone i raportuje do Rollbara. + +!!! note "Obraz backup-runnera — bez podwójnego ściągania" + Domyślnie `backup-runner` używa **tego samego** obrazu co `dbserver` + (`postgres:${DJANGO_BPP_POSTGRESQL_VERSION}`, wariant Debian) — dzięki temu + współdzieli z nim 100% warstw i nie zajmuje dodatkowego miejsca na dysku + (osobny `-alpine` nie dzieli warstw z Debianem i kosztowałby ~350 MB więcej). + `pg_dump` trafia dokładnie w wersję serwera. `rclone`, `curl`, `jq` są + doinstalowane w runtime (`apt-get`). W trybie **zewnętrznej bazy** `dbserver` + to lekki sentinel `postgres:-alpine`; tam `init-configs` ustawia + `BPP_BACKUP_PG_IMAGE=postgres:-alpine`, by `backup-runner` współdzielił + warstwy z sentinelem (na starych instalacjach dopisuje to `ensure-config-files` + przy zwykłym `make up`). `make backup-cycle` uruchamia ten sam cykl ręcznie. diff --git a/docs/eksploatacja/komendy.md b/docs/eksploatacja/komendy.md index 2c870f5..2be405c 100644 --- a/docs/eksploatacja/komendy.md +++ b/docs/eksploatacja/komendy.md @@ -10,14 +10,25 @@ make help # Pełna lista wszystkich targetów Make (źródło prawdy ```bash make run # Pełne wdrożenie (pull, build, configs, up) -make up # Start wszystkich usług (force recreate) -make up-quick # Szybki start bez recreation +make up # Start wszystkich usług (force recreate) + sprzątanie Dockera +make up-quick # Szybki start bez recreation (bez sprzątania) make refresh # prune + pull + recreate (po update obrazu) make wait # Czeka na build z GH Actions, potem make refresh make stop # Zatrzymaj usługi make restart-appserver # Restart serwera aplikacji ``` +!!! note "Sprzątanie Dockera po `make up` / `make run`" + Po **udanym** starcie (`--wait` — wszystkie usługi zdrowe) `make up` (a więc i + `make run`) uruchamia `docker system prune -af` i wypisuje tylko ile miejsca + zwolniono (`Zwolniono na dysku: …`). Usuwa to nieużywane obrazy (w tym stare + wersje obrazów BPP po aktualizacji), zatrzymane kontenery, niepodpięte sieci i + cache builda. **Bez `--volumes`** — nazwane wolumeny z danymi (`postgresql_data`, + `media`, `staticfiles`) są bezpieczne. Uwaga: `-af` usuwa **wszystkie** nieużywane + obrazy na hoście, także spoza BPP — na maszynie współdzielonej z innymi projektami + używaj `make up-quick` (nie sprząta). Obraz fallback `iplweb/html2docx` jest + pobierany **po** prune, więc nie znika. + ## Baza danych ```bash diff --git a/docs/konfiguracja/postgresql.md b/docs/konfiguracja/postgresql.md index a024970..49ef63d 100644 --- a/docs/konfiguracja/postgresql.md +++ b/docs/konfiguracja/postgresql.md @@ -1,9 +1,17 @@ # PostgreSQL — wersje i upgrade Kontener `dbserver` używa **oficjalnego obrazu** `postgres:${DJANGO_BPP_POSTGRESQL_VERSION}` -(wariant Debian, nie `-alpine`), format `MAJOR.MINOR` (np. `16.13`, `17.9`, `18.3`). Wersja -jest sterowana zmienną `DJANGO_BPP_POSTGRESQL_VERSION` w `$BPP_CONFIGS_DIR/.env`. Domyślnie -`16.13`. +(wariant Debian, nie `-alpine`), format `MAJOR.MINOR` (np. `18.4`, `17.9`, `16.13`). Wersja +jest sterowana zmienną `DJANGO_BPP_POSTGRESQL_VERSION` w `$BPP_CONFIGS_DIR/.env`. Nowe +instalacje dostają domyślnie **`18.4`** (najnowsza wersja z gałęzi 18). + +!!! warning "Domyślna `18.4` dotyczy tylko NOWYCH instalacji" + `init-configs` wpisuje `18.4` przy pierwszym uruchomieniu. Istniejące instalacje + zachowują swoją wersję z `.env` — **upgrade majora nigdy nie dzieje się sam** + (wymaga dump/restore przez `make upgrade-postgres`). Fallback w `docker-compose` + (`:-16.13`) celowo **pozostaje na `16.13`** jako siatka bezpieczeństwa dla + pradawnych `.env` bez tej zmiennej — gdyby skoczył na `18.4`, taki klaster PG16 + dostałby obraz PG18 na danych PG16 i nie wstałby. > **Skąd autotune?** Wcześniej `dbserver` używał własnego obrazu `iplweb/bpp_dbserver` — > jest on **wycofany**, a jego jedynym dodatkiem ponad stockowego postgresa był *autotune*. @@ -13,12 +21,17 @@ jest sterowana zmienną `DJANGO_BPP_POSTGRESQL_VERSION` w `$BPP_CONFIGS_DIR/.env > do limitu pamięci kontenera (`DBSERVER_MEM_LIMIT`, ~95%) i startuje normalnie. Bez buildu, > bez `python3`. Szczegóły strojenia: [Limity zasobów](limity-zasobow.md). -`DJANGO_BPP_POSTGRESQL_VERSION_MAJOR` (auto-derived z `_VERSION`) jest używana przez -`backup-runner` (`postgres:-alpine` — `pg_dump` musi być ≥ wersji serwera). -W trybie external obie zmienne trzymają tylko major. +`DJANGO_BPP_POSTGRESQL_VERSION_MAJOR` (auto-derived z `_VERSION`) trzyma sam major. +W trybie lokalnym `backup-runner` używa jednak tego **samego pełnego obrazu** co +`dbserver` (`postgres:${DJANGO_BPP_POSTGRESQL_VERSION}`, Debian) — współdzieli z nim +warstwy zamiast ściągać osobny `-alpine`; `pg_dump` trafia dokładnie w wersję serwera. +W trybie external `dbserver` to sentinel `postgres:-alpine` i wtedy zmienna +`BPP_BACKUP_PG_IMAGE` kieruje `backup-runner` na ten sam alpine. `_MAJOR` nadal +napędza tag sentinela oraz krok upgrade'u. Szczegóły: +[Backup i rclone](../eksploatacja/backup-i-rclone.md). Wybór wersji następuje przy pierwszym uruchomieniu `make` — `init-configs` zapyta -`Wersja PostgreSQL [16.13]:`. Lista tagów: +`Wersja PostgreSQL [18.4]:`. Lista tagów: [hub.docker.com/_/postgres](https://hub.docker.com/_/postgres). !!! note "Kolacja (sortowanie) i PGDATA" diff --git a/mk/deployment.mk b/mk/deployment.mk index 84fac3c..1dcb54c 100644 --- a/mk/deployment.mk +++ b/mk/deployment.mk @@ -40,6 +40,16 @@ build: up: validate-env-quotes ensure-config-files update-configs docker compose up -d --wait --force-recreate --remove-orphans @bash scripts/create-monitoring-user.sh --soft + @# Stack jest UP i healthy (--wait powyzej; przy bledzie make juz by stanal). + @# Sprzatamy nieuzywane obrazy/kontenery/sieci/cache Dockera. BEZ --volumes, + @# wiec nazwane wolumeny (postgresql_data, media, staticfiles) sa BEZPIECZNE. + @# -af kasuje WSZYSTKIE nieuzywane obrazy (tez stare wersje BPP, osierocony + @# alpine po przejsciu backup-runnera na obraz dbservera). Pokazujemy tylko + @# ile miejsca zwolniono. Robimy to PRZED pullem html2docx, by go nie usunac. + @echo "Sprzatanie Dockera (docker system prune -af)..." + @docker system prune -af 2>/dev/null \ + | awk -F': ' '/Total reclaimed space/ {print " Zwolniono na dysku: " $$2}' \ + || true @if [ "$(DJANGO_BPP_ENABLE_HTML2DOCX_IMAGE)" = "true" ]; then \ docker pull iplweb/html2docx:latest; \ fi diff --git a/scripts/ensure-config-files.sh b/scripts/ensure-config-files.sh index 6326455..0bdafdd 100755 --- a/scripts/ensure-config-files.sh +++ b/scripts/ensure-config-files.sh @@ -173,6 +173,23 @@ if [ -f "$_ENV" ]; then # bez koniecznosci recznego `make init-configs`. _ensure_var DJANGO_BPP_MEDIA_ROOT "/mediaroot" \ " + dopisano brakujace DJANGO_BPP_MEDIA_ROOT=/mediaroot w .env" + + # backup-runner image override - TYLKO w trybie zewnetrznej bazy. Tam + # dbserver to lekki sentinel postgres:-alpine, wiec backup-runner ma + # wspoldzielic z nim warstwy (zamiast ciagnac osobny obraz Debianowy ~450MB). + # W trybie lokalnym ta zmienna POZOSTAJE nieustawiona -> compose bierze pelny + # obraz dbservera (postgres:), wspoldzielony z prawdziwym PG. + # Stare instalacje external (sprzed tej zmiennej) dostaja ja tu, bez + # recznego kroku - zgodnie z regula kompatybilnosci wstecznej. + _DBCOMPOSE="$(grep -E '^BPP_DATABASE_COMPOSE=' "$REPO_DIR/.env" 2>/dev/null | tail -1 | cut -d= -f2- || true)" + if [ "$_DBCOMPOSE" = "docker-compose.database.external.yml" ]; then + _bk_major="$(grep -E '^DJANGO_BPP_POSTGRESQL_VERSION_MAJOR=' "$_ENV" 2>/dev/null | tail -1 | cut -d= -f2-)" + [ -n "$_bk_major" ] || _bk_major="$(grep -E '^DJANGO_BPP_POSTGRESQL_DB_VERSION=' "$_ENV" 2>/dev/null | tail -1 | cut -d= -f2-)" + if [ -n "$_bk_major" ]; then + _ensure_var BPP_BACKUP_PG_IMAGE "postgres:${_bk_major}-alpine" \ + " + dopisano BPP_BACKUP_PG_IMAGE=postgres:${_bk_major}-alpine (tryb external) w .env" + fi + fi fi if [ -f "$_ENV" ] && [ -f "$DEFAULTS_DIR/netdata/go.d/postgres.conf.tpl" ]; then diff --git a/scripts/init-configs.sh b/scripts/init-configs.sh index 2297ab1..5e05852 100755 --- a/scripts/init-configs.sh +++ b/scripts/init-configs.sh @@ -284,9 +284,9 @@ if [ ! -f "$ENV_FILE" ]; then EXT_PG_VERSION="$DETECTED_PG_MAJOR" else echo "UWAGA: nie udalo sie wykryc wersji (brak polaczenia, bledna auth lub firewall)." - printf "Podaj wersje major PostgreSQL recznie [17]: " + printf "Podaj wersje major PostgreSQL recznie [18]: " read -r EXT_PG_VERSION || true - EXT_PG_VERSION="${EXT_PG_VERSION:-17}" + EXT_PG_VERSION="${EXT_PG_VERSION:-18}" fi echo "" else @@ -300,10 +300,10 @@ if [ ! -f "$ENV_FILE" ]; then echo "=== Wersja PostgreSQL dla dbserver ===" echo "Kontener dbserver uzywa oficjalnego obrazu postgres: + autotune." echo "Dostepne tagi: https://hub.docker.com/_/postgres" - echo "Przyklady: 16.13, 17.9, 18.3 (zalecany format MAJOR.MINOR)." - printf "Wersja PostgreSQL [16.13]: " + echo "Przyklady: 18.4, 17.9, 16.13 (zalecany format MAJOR.MINOR)." + printf "Wersja PostgreSQL [18.4]: " read -r DBSERVER_PG_VERSION || true - DBSERVER_PG_VERSION="${DBSERVER_PG_VERSION:-16.13}" + DBSERVER_PG_VERSION="${DBSERVER_PG_VERSION:-18.4}" # Wyciagnij major (16.13 -> 16) jako domyslny dla backup-runnera, # zeby out-of-the-box pg_dump byl tej samej wersji co serwer. DBSERVER_PG_MAJOR="${DBSERVER_PG_VERSION%%.*}" @@ -414,6 +414,10 @@ EOF # dla spojnosci z trybem lokalnym (gdzie VERSION jest MAJOR.MINOR). DJANGO_BPP_POSTGRESQL_VERSION=$EXT_PG_VERSION DJANGO_BPP_POSTGRESQL_VERSION_MAJOR=$EXT_PG_VERSION +# Tryb external: backup-runner ma wspoldzielic lekki obraz z sentinelem +# (postgres:-alpine) zamiast ciagnac osobny obraz Debianowy. W trybie +# lokalnym ta zmienna jest NIEUSTAWIONA -> compose bierze pelny obraz dbservera. +BPP_BACKUP_PG_IMAGE=postgres:$EXT_PG_VERSION-alpine EOF fi