From 38b8371e7e7a2d36b159f17544f21ac7863790d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Fri, 12 Jun 2026 14:33:48 +0200 Subject: [PATCH 01/11] =?UTF-8?q?spec:=20design=20test-upgrade=20(pr=C3=B3?= =?UTF-8?q?ba=20generalna=20migracji)=20+=20zaspawaj-wersje=20(pin=20DOCKE?= =?UTF-8?q?R=5FVERSION)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- ...-12-test-upgrade-zaspawaj-wersje-design.md | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-12-test-upgrade-zaspawaj-wersje-design.md diff --git a/docs/superpowers/specs/2026-06-12-test-upgrade-zaspawaj-wersje-design.md b/docs/superpowers/specs/2026-06-12-test-upgrade-zaspawaj-wersje-design.md new file mode 100644 index 0000000..401f9cf --- /dev/null +++ b/docs/superpowers/specs/2026-06-12-test-upgrade-zaspawaj-wersje-design.md @@ -0,0 +1,192 @@ +# Design: `make test-upgrade` (próba generalna migracji) + `make zaspawaj-wersje` (pinowanie DOCKER_VERSION) + +Data: 2026-06-12 +Status: zaakceptowany kierunek, do implementacji + +## Kontekst i motywacja + +Docelowo bpp-deploy ma dostać automatyczny, zdalny deployment w modelu pull +(systemd timer na hoście wykrywa nowy release-tag → pipeline: kotwica → pull → +backup → próba generalna → `make run` → zaspawanie wersji → ntfy; awaria = +stop + alert + sesja tmux do wglądu). Ten spec realizuje **dwa pierwsze, +samodzielnie użyteczne klocki** tego pipeline'u jako ręczne cele make: + +1. **`test-upgrade`** — próba generalna: czy migracje obrazu-kandydata + przechodzą na kopii produkcyjnej bazy, bez dotykania działającego stacka. +2. **`zaspawaj-wersje`** — utrwalenie w `.env` wersji obrazów iplweb, na + której faktycznie chodzi produkcja (`DOCKER_VERSION=` zamiast + ruchomego `latest`). + +Główne ryzyko, które adresujemy: "zbuduje się kupa" — nowy obraz z migracją, +która nie przechodzi, wykładał produkcję dopiero w trakcie deployu. Po tej +zmianie padnięta migracja zostaje wykryta na boku, na świeżej kopii +produkcyjnych danych, zanim ktokolwiek dotknie działających usług. + +## Zakres + +**W zakresie:** + +- Target `make test-upgrade [TAG=...]` + skrypt `scripts/test-upgrade.sh`. +- Target `make test-upgrade-clean` (sprzątanie shadow stacka po awarii). +- Target `make zaspawaj-wersje [TAG=...]` + skrypt `scripts/zaspawaj-wersje.sh`. +- Wspólny helper rozwiązywania wersji (digest ↔ tag CalVer przez API Docker Huba). +- Dokumentacja (docs/eksploatacja/komendy.md + pomoc `make help`). + +**Poza zakresem (przyszłe etapy):** + +- Auto-deploy (systemd timer, tmux, ntfy, lockfile, kotwica, rollback). +- Pinowanie wersji w release'ach bpp-deploy (release deklaruje DOCKER_VERSION). +- Zmiany w `make release` / `wait-for-build.sh`. + +## Cel 1: `make test-upgrade` + +``` +make test-upgrade [TAG=202606.1386] +``` + +### Przebieg + +1. **Rozwiązanie kandydata.** Bez `TAG`: najnowszy tag CalVer + (wzorzec `^[0-9]{6}\.[0-9]+$`) z API Docker Huba dla `iplweb/bpp_appserver`. + Obraz jest pullowany **po tagu wersji, nie przez `:latest`** — lokalny tag + `latest`, na którym chodzi produkcja, pozostaje nietknięty (jedyny "cichy" + kanał nadpisania produkcji zostaje zamknięty). +2. **Backup.** `make db-backup` (istniejący: `pg_dump -Fd -j4` → tar.gz w + `$DJANGO_BPP_HOST_BACKUP_DIR`). Błąd backupu = twarde przerwanie — + zasada "bez kotwicy nie ruszamy dalej". +3. **Shadow stack** — poza projektem Compose, na dedykowanej sieci docker + `bpp-shadow` (czysty `docker run`, prefiks nazw `bpp-shadow-*`): + - `bpp-shadow-dbserver`: `iplweb/bpp_dbserver:psql-$DJANGO_BPP_POSTGRESQL_VERSION` + (ten sam obraz co produkcja — już lokalnie obecny), tymczasowy wolumen, + te same `DJANGO_BPP_DB_USER/PASSWORD/NAME` co produkcja (zgodność ról + przy restore), limity zasobów przycięte (RAM/CPU), czekanie na + `pg_isready`. + - `bpp-shadow-redis`: `redis:8.6.2` (ta sama wersja co w + `docker-compose.infrastructure.yml`). +4. **Restore dumpa** do shadow-bazy: untar + `pg_restore -Fd -j N --no-owner`. +5. **Migracja kandydatem** — entrypoint override, nic poza migracją się nie + uruchamia: + + ``` + docker run --rm --network bpp-shadow \ + -e DJANGO_BPP_DB_HOST=bpp-shadow-dbserver \ + -e DJANGO_BPP_REDIS_HOST=bpp-shadow-redis \ + (… reszta wymaganych env z $BPP_CONFIGS_DIR/.env …) \ + --entrypoint python \ + iplweb/bpp_appserver: src/manage.py migrate --noinput + ``` + +6. **Wynik:** + - **Sukces** → komunikat "migracje \ przechodzą na kopii + produkcyjnej bazy", pełne sprzątanie (kontenery + wolumen + sieć), + exit 0. + - **Porażka** → ogon logu migracji, **shadow stack zostaje** do ręcznej + inspekcji (`docker exec`/`psql`), instrukcja sprzątnięcia + (`make test-upgrade-clean`), exit 1. + +Exit code czyni target komponowalnym — w przyszłym auto-deployu będzie +krokiem "próba generalna" bez zmian. + +### Gwarancje nienaruszalności produkcji + +- Zero operacji na kontenerach/wolumenach projektu Compose. +- Zero zmian lokalnego tagu `latest` (pull wyłącznie po tagu wersji). +- Zero zapisu do `$BPP_CONFIGS_DIR/.env`. +- Jedyne obciążenie: CPU/IO podczas dump+restore oraz dysk ≈ rozmiar bazy + (sprawdzany przed startem; brak miejsca = czytelne przerwanie). + +### `make test-upgrade-clean` + +Idempotentne usunięcie `bpp-shadow-*` (kontenery, wolumen, sieć). Wywoływane +też automatycznie na początku `test-upgrade` (zombie z poprzedniego przebiegu). + +## Cel 2: `make zaspawaj-wersje` + +``` +make zaspawaj-wersje [TAG=202606.1386] +``` + +### Przebieg + +1. Bez `TAG`: odczytaj digest obrazu z **działającego kontenera** `appserver` + (nie z lokalnego tagu `latest` — po `make pull` bez recreate lokalny + `latest` może już wskazywać nowszy, nieprzetestowany obraz; spawamy stan + faktyczny produkcji). +2. Rozwiąż digest → tag CalVer przez API Docker Huba. +3. Sanity-check: pozostałe kontenery iplweb (`authserver`, `workerserver`, + `denorm-queue`, `celerybeat`) chodzą na tej samej wersji? Rozjazd → + ostrzeżenie (spawamy wg appservera). +4. `set_env_var DOCKER_VERSION=` w `$BPP_CONFIGS_DIR/.env` — istniejące + stabilne helpery (`env_has_var`/`get_env_var`/`set_env_var`), nie własny sed. +5. Komunikat końcowy; **nic nie jest restartowane** — pin obowiązuje od + następnej operacji compose. + +Z `TAG=` target jest też ręczną ścieżką aktualizacji na zaspawanym hoście: +`make zaspawaj-wersje TAG= && make pull && make up`. + +### Zasięg zmiennej + +`DOCKER_VERSION` steruje dokładnie pięcioma obrazami iplweb: `bpp_appserver`, +`bpp_authserver`, `bpp_workerserver`, `bpp_denorm_queue`, `bpp_beatserver`. +Pozostałe obrazy (nginx, redis, grafana, netdata, ofelia, certbot, …) są już +przypięte na sztywno w plikach compose; dbserver ma własną +`DJANGO_BPP_POSTGRESQL_VERSION`. Target nie dotyka niczego poza +`DOCKER_VERSION`. + +### Tryby błędu + +- Kontener `appserver` nie działa → błąd z podpowiedzią (`TAG=` albo `make up`). +- Digest nieznany w Hubie (obraz budowany lokalnie, bardzo stary tag) lub brak + sieci → czytelny błąd, `.env` nietknięty. +- `DOCKER_VERSION` już ustawiony → nadpisanie z komunikatem + (target jest idempotentny: "przybij to, co chodzi"). + +## Wspólny helper: rozwiązywanie wersji + +Oba cele potrzebują mapowania digest ↔ tag CalVer. Wspólna funkcja (w +`scripts/`, source'owana przez oba skrypty): + +- `resolve_latest_calver(repo)` → najnowszy tag `^[0-9]{6}\.[0-9]+$` z + `hub.docker.com/v2/repositories/iplweb//tags`. +- `resolve_digest_to_calver(repo, digest)` → tag CalVer o tym samym digeście. + +Zależności: `curl` + `jq` (jq jest już zależnością `wait-for-build.sh`). +Implementacja może dodatkowo preferować etykietę OCI +`org.opencontainers.image.version`, jeśli obrazy ją niosą (do weryfikacji +w trakcie implementacji); API Huba pozostaje ścieżką gwarantowaną. + +## Kompatybilność wsteczna + +- Żadnych nowych wymaganych zmiennych. Domyślka compose + `${DOCKER_VERSION:-latest}` zostaje — host bez zaspawania działa jak dotąd. +- `zaspawaj-wersje` jest opt-in per host; `.env` modyfikowany wyłącznie + stabilnymi helperami. +- `test-upgrade` niczego nie zmienia w konfiguracji — jest czystym odczytem + (plus dump, który i tak jest standardową operacją). + +## Testy + +- Skrypty z testami jednostkowymi w konwencji repo + (`scripts/test-letsencrypt.sh`-style: mockowany `docker`/`curl`, bez sieci) + dla logiki rozwiązywania wersji i ścieżek błędów. +- Test manualny na hoście stagingowym: pełny przebieg `test-upgrade` + (sukces + symulowana porażka migracji), `zaspawaj-wersje` na działającym + stacku. + +## Dokumentacja (docs-sync) + +- `docs/eksploatacja/komendy.md`: sekcje dla obu targetów. +- `make help`: wpisy w sekcjach Deployment/Konfiguracja. +- `CLAUDE.md`: wzmianka o pinowaniu DOCKER_VERSION (kontrakt: latest vs pin). +- `mkdocs build --strict` po zmianach. + +## Roadmapa (kontekst, poza tym specem) + +Kolejność dalszych etapów uzgodniona w dyskusji 2026-06-12: + +1. ten spec (klocki ręczne), +2. auto-deploy pull-based: timer systemd + tmux + ntfy + lockfile + kotwica + (stary kod wykonuje pre-flight: kotwica → pull → backup → test-upgrade; + `git checkout ` dopiero po udanej próbie; `make run` nowym kodem; + zaspawanie po sukcesie), +3. ewentualnie: release bpp-deploy deklarujący wersję obrazów. From 0dc0d31ddf4b3a68e5a501fd330a4e9623755870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Fri, 12 Jun 2026 14:51:05 +0200 Subject: [PATCH 02/11] plan: implementacja test-upgrade + zaspawaj-wersje Co-Authored-By: Claude Fable 5 --- ...2026-06-12-test-upgrade-zaspawaj-wersje.md | 1059 +++++++++++++++++ 1 file changed, 1059 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-12-test-upgrade-zaspawaj-wersje.md diff --git a/docs/superpowers/plans/2026-06-12-test-upgrade-zaspawaj-wersje.md b/docs/superpowers/plans/2026-06-12-test-upgrade-zaspawaj-wersje.md new file mode 100644 index 0000000..4f6c55a --- /dev/null +++ b/docs/superpowers/plans/2026-06-12-test-upgrade-zaspawaj-wersje.md @@ -0,0 +1,1059 @@ +# `make test-upgrade` + `make zaspawaj-wersje` Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Dwa ręczne cele make: `test-upgrade` (próba generalna migracji obrazu-kandydata na kopii produkcyjnej bazy, bez dotykania produkcji) i `zaspawaj-wersje` (pinowanie `DOCKER_VERSION` w `.env` do wersji, na której faktycznie chodzi appserver). + +**Architecture:** Wspólna biblioteka shellowa (`lib-docker-versions.sh`) mapuje digesty obrazów ↔ tagi CalVer przez API Docker Huba. `zaspawaj-wersje.sh` zapisuje wynik do `.env` stabilnym helperem `set_env_var`. `test-upgrade.sh` stawia shadow stack (dbserver+redis) czystym `docker run` poza projektem Compose, restoruje świeży dump i odpala `manage.py migrate` obrazem-kandydatem z nadpisanym entrypointem. Spec: `docs/superpowers/specs/2026-06-12-test-upgrade-zaspawaj-wersje-design.md`. + +**Tech Stack:** bash (BSD/GNU-portable — testy biegają też na macOS), curl + jq, docker / docker compose, GNU make. Testy jednostkowe w konwencji repo (`scripts/test-letsencrypt.sh`): mockowane `curl`/`docker` w PATH, zero sieci, zero Docker daemona. + +--- + +## File Structure + +| Plik | Odpowiedzialność | +|---|---| +| `scripts/lib-docker-versions.sh` (create) | Czyste funkcje: `resolve_latest_calver`, `resolve_digest_to_calver`, `verify_tag_exists`, `running_repo_digest`. Source'owana, bez side-effectów. | +| `scripts/test-docker-versions.sh` (create) | Testy jednostkowe lib + `zaspawaj-wersje.sh` + `test-upgrade.sh --clean` (mock curl/docker). | +| `scripts/zaspawaj-wersje.sh` (create) | Pinowanie `DOCKER_VERSION` w `.env` (z `TAG=` lub z działającego kontenera). | +| `scripts/test-upgrade.sh` (create) | Próba generalna + `--clean`. | +| `mk/configs.mk` (modify) | Target `zaspawaj-wersje`. | +| `mk/misc.mk` (modify) | Target `test-docker-versions`. | +| `mk/deployment.mk` (modify) | Targety `test-upgrade`, `test-upgrade-clean`. | +| `Makefile` (modify) | Wpisy w `make help`. | +| `docs/eksploatacja/komendy.md` (modify) | Sekcja operatorska dla obu celów. | +| `CLAUDE.md` (modify) | Kontrakt pinowania `DOCKER_VERSION` (agent steering). | + +Konwencje repo, których trzymamy się wszędzie: + +- Helpery `.env` (`env_has_var`/`get_env_var`/`set_env_var`) są **kopiowane per-skrypt** (precedens: `scripts/configure-resources.sh`), nie source'owane z `init-configs.sh` (tamten skrypt wykonuje się przy source). +- Komunikaty PL bez ogonków w skryptach (konwencja: `init-configs.sh`, `test-letsencrypt.sh`). +- BSD/GNU-portability: `grep -E`, `df -Pm`, bez bashowych tablic asocjacyjnych (macOS ma bash 3.2). +- Po edycji docs: `mkdocs build --strict`. + +--- + +### Task 1: Biblioteka `lib-docker-versions.sh` (TDD) + +**Files:** +- Create: `scripts/test-docker-versions.sh` +- Create: `scripts/lib-docker-versions.sh` + +- [ ] **Step 1: Napisz failing testy (harness + testy samej lib)** + +Utwórz `scripts/test-docker-versions.sh` (chmod +x): + +```bash +#!/usr/bin/env bash +# +# Testy scripts/lib-docker-versions.sh, scripts/zaspawaj-wersje.sh oraz +# scripts/test-upgrade.sh (sciezka --clean). +# +# Bez sieci i bez Docker daemona: `curl` i `docker` sa mockowane +# stub-skryptami w PATH (konwencja: scripts/test-letsencrypt.sh). +# +# Uruchomienie: `make test-docker-versions` lub +# `bash scripts/test-docker-versions.sh` + +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" +LIB="$REPO_DIR/scripts/lib-docker-versions.sh" + +TEST_ROOT="$(mktemp -d -t bpp-docker-versions-test-XXXXXX)" +MOCK_BIN="$TEST_ROOT/mock-bin" +CURL_LOG="$TEST_ROOT/curl-calls.log" +DOCKER_LOG="$TEST_ROOT/docker-calls.log" +FIXTURES="$TEST_ROOT/fixtures" +mkdir -p "$MOCK_BIN" "$FIXTURES" + +# shellcheck disable=SC2317 # wywolywane przez trap +cleanup() { rm -rf "$TEST_ROOT"; } +trap cleanup EXIT + +green() { printf "\033[32m%s\033[0m\n" "$*"; } +red() { printf "\033[31m%s\033[0m\n" "$*"; } +PASS=0; FAIL=0 +pass() { green " PASS: $1"; PASS=$((PASS + 1)); } +fail() { red " FAIL: $1"; FAIL=$((FAIL + 1)); } + +assert_eq() { + local expected="$1" actual="$2" name="$3" + if [ "$expected" = "$actual" ]; then pass "$name"; else + fail "$name (oczekiwane '$expected', otrzymano '$actual')"; fi +} +assert_exit() { + local expected="$1" actual="$2" name="$3" + if [ "$expected" = "$actual" ]; then pass "$name"; else + fail "$name (oczekiwany exit=$expected, otrzymany exit=$actual)"; fi +} +assert_nonzero() { + local actual="$1" name="$2" + if [ "$actual" != "0" ]; then pass "$name"; else + fail "$name (oczekiwany exit != 0, otrzymany 0)"; fi +} +assert_file_contains() { + local file="$1" needle="$2" name="$3" + if grep -qF -- "$needle" "$file" 2>/dev/null; then pass "$name"; else + fail "$name (brak '$needle' w $file)"; fi +} +assert_file_not_contains() { + local file="$1" needle="$2" name="$3" + if grep -qF -- "$needle" "$file" 2>/dev/null; then + fail "$name ('$needle' obecne w $file)"; else pass "$name"; fi +} + +# --- Mock curl --- +# Ostatni argument wywolania = URL. +# .../tags?page_size=... -> cat $CURL_FIXTURE (lista tagow) +# .../tags/ -> exit 0 gdy CURL_TAG_EXISTS=1 (domyslnie), inaczej 22 +cat > "$MOCK_BIN/curl" <> "$CURL_LOG" +case "\$last" in + *"/tags?page_size="*) + if [ -n "\${CURL_FIXTURE:-}" ]; then cat "\$CURL_FIXTURE"; else exit 22; fi ;; + *"/tags/"*) + [ "\${CURL_TAG_EXISTS:-1}" = "1" ] || exit 22 ;; + *) exit 22 ;; +esac +EOF +chmod +x "$MOCK_BIN/curl" + +# --- Mock docker --- +# compose ps -q appserver -> cid (pusty gdy MOCK_APPSERVER_RUNNING=0) +# compose ps -q -> cid-other +# inspect --format {{.Image}} cid-* -> img-123 +# image inspect --format ... img-123 -> repo digest (MOCK_RUNNING_DIGEST) +cat > "$MOCK_BIN/docker" <> "$DOCKER_LOG" +case "\$*" in + "compose ps -q appserver") + if [ "\${MOCK_APPSERVER_RUNNING:-1}" = "1" ]; then echo "cid-app"; fi ;; + "compose ps -q "*) + echo "cid-other" ;; + "inspect --format {{.Image}} cid-"*) + echo "img-123" ;; + "image inspect --format "*) + echo "iplweb/bpp_appserver@\${MOCK_RUNNING_DIGEST:-sha256:aaa}" ;; + *) exit 0 ;; +esac +EOF +chmod +x "$MOCK_BIN/docker" + +export PATH="$MOCK_BIN:$PATH" + +# --- Fixtures API Docker Huba --- +cat > "$FIXTURES/tags.json" <<'EOF' +{"results":[ + {"name":"latest","digest":"sha256:aaa","images":[{"digest":"sha256:arm64aaa"},{"digest":"sha256:amd64aaa"}]}, + {"name":"feature-multi-hosted-config","digest":"sha256:fff","images":[]}, + {"name":"sha-56127ac","digest":"sha256:aaa","images":[]}, + {"name":"202606.1386","digest":"sha256:aaa","images":[{"digest":"sha256:arm64aaa"},{"digest":"sha256:amd64aaa"}]}, + {"name":"202606.999","digest":"sha256:bbb","images":[{"digest":"sha256:armbbb"}]}, + {"name":"202605.1300","digest":"sha256:ccc","images":[]} +]} +EOF +cat > "$FIXTURES/no-calver.json" <<'EOF' +{"results":[{"name":"latest","digest":"sha256:aaa","images":[]}]} +EOF + +# ===================== Testy lib-docker-versions.sh ===================== +echo "== lib-docker-versions.sh ==" +# shellcheck source=/dev/null +. "$LIB" + +export CURL_FIXTURE="$FIXTURES/tags.json" + +out="$(resolve_latest_calver iplweb/bpp_appserver)" +assert_eq "202606.1386" "$out" "resolve_latest_calver: najnowszy numerycznie (1386 > 999)" + +out="$(resolve_digest_to_calver iplweb/bpp_appserver sha256:aaa)" +assert_eq "202606.1386" "$out" "resolve_digest_to_calver: digest manifest-list" + +out="$(resolve_digest_to_calver iplweb/bpp_appserver sha256:armbbb)" +assert_eq "202606.999" "$out" "resolve_digest_to_calver: digest per-arch (images[])" + +rc=0; resolve_digest_to_calver iplweb/bpp_appserver sha256:zzz >/dev/null 2>&1 || rc=$? +assert_nonzero "$rc" "resolve_digest_to_calver: nieznany digest -> exit != 0" + +CURL_FIXTURE="$FIXTURES/no-calver.json" +rc=0; resolve_latest_calver iplweb/bpp_appserver >/dev/null 2>&1 || rc=$? +assert_nonzero "$rc" "resolve_latest_calver: brak tagow CalVer -> exit != 0" +CURL_FIXTURE="$FIXTURES/tags.json" + +rc=0; verify_tag_exists iplweb/bpp_appserver 202606.1386 || rc=$? +assert_exit 0 "$rc" "verify_tag_exists: istniejacy tag -> exit 0" + +rc=0; CURL_TAG_EXISTS=0 verify_tag_exists iplweb/bpp_appserver 999999.1 || rc=$? +assert_nonzero "$rc" "verify_tag_exists: brak tagu -> exit != 0" + +out="$(running_repo_digest appserver)" +assert_eq "sha256:aaa" "$out" "running_repo_digest: digest z dzialajacego kontenera" + +rc=0; MOCK_APPSERVER_RUNNING=0 running_repo_digest appserver >/dev/null 2>&1 || rc=$? +assert_nonzero "$rc" "running_repo_digest: kontener nie dziala -> exit != 0" + +# ======================= Podsumowanie ======================= +echo "" +echo "Wynik: PASS=$PASS FAIL=$FAIL" +[ "$FAIL" -eq 0 ] +``` + +(Testy `zaspawaj-wersje.sh` i `--clean` dojdą w Taskach 2 i 4 — sekcje wstawiane PRZED blokiem `Podsumowanie`.) + +- [ ] **Step 2: Uruchom testy — mają polec (brak lib)** + +Run: `bash scripts/test-docker-versions.sh` +Expected: błąd przy `. "$LIB"` (No such file or directory), exit != 0. + +- [ ] **Step 3: Zaimplementuj `scripts/lib-docker-versions.sh`** + +```bash +#!/usr/bin/env bash +# Wspolne funkcje mapowania digest <-> tag CalVer dla obrazow iplweb/* na +# Docker Hubie. Source'owana przez scripts/test-upgrade.sh i +# scripts/zaspawaj-wersje.sh - bez side-effectow przy source. +# Zaleznosci: curl, jq, docker (tylko running_repo_digest). +# +# Tagi CalVer obrazow BPP: YYYYMM.NNNN (np. 202606.1386). `latest` na Hubie +# wskazuje ten sam digest co najnowszy tag CalVer. + +# Wzorzec tagu CalVer (BSD/GNU `grep -E` compatible). +CALVER_RE='^[0-9]{6}\.[0-9]+$' + +# Endpoint API - nadpisywalny w testach. +HUB_API="${HUB_API:-https://hub.docker.com/v2}" + +# _hub_tags_json -- surowy JSON pierwszych 100 tagow repozytorium. +_hub_tags_json() { + curl -fsS "$HUB_API/repositories/$1/tags?page_size=100" +} + +# resolve_latest_calver +# stdout: najnowszy (numerycznie) tag CalVer; exit 1 gdy brak / blad sieci. +resolve_latest_calver() { + local repo="$1" tag + tag="$(_hub_tags_json "$repo" \ + | jq -r '.results[].name' \ + | grep -E "$CALVER_RE" \ + | sort -t. -k1,1n -k2,2n \ + | tail -1)" || true + if [ -z "$tag" ]; then + echo "BLAD: nie znaleziono tagu CalVer dla $repo (siec? API Huba?)" >&2 + return 1 + fi + printf '%s\n' "$tag" +} + +# resolve_digest_to_calver +# stdout: tag CalVer o tym digescie (manifest-list LUB per-arch z .images[]); +# exit 1 gdy nie znaleziono. +resolve_digest_to_calver() { + local repo="$1" digest="$2" tag + tag="$(_hub_tags_json "$repo" \ + | jq -r --arg d "$digest" \ + '.results[] + | select(((.digest // "") == $d) + or (([.images[]?.digest // empty] | index($d)) != null)) + | .name' \ + | grep -E "$CALVER_RE" \ + | head -1)" || true + if [ -z "$tag" ]; then + echo "BLAD: digest $digest nie odpowiada zadnemu tagowi CalVer w $repo" >&2 + return 1 + fi + printf '%s\n' "$tag" +} + +# verify_tag_exists -- exit 0 gdy tag istnieje na Hubie. +verify_tag_exists() { + curl -fsS "$HUB_API/repositories/$1/tags/$2" >/dev/null 2>&1 +} + +# running_repo_digest +# stdout: digest (sha256:...) obrazu, na ktorym CHODZI kontener uslugi - +# celowo nie z lokalnego tagu :latest (po `make pull` bez recreate lokalny +# tag moze juz wskazywac nowszy obraz niz dzialajacy kontener). +# Wymaga CWD = katalog repo (docker compose). exit 1 gdy kontener nie dziala +# albo obraz nie ma RepoDigests (np. budowany lokalnie). +running_repo_digest() { + local svc="$1" cid img digest + cid="$(docker compose ps -q "$svc" 2>/dev/null | head -1)" + if [ -z "$cid" ]; then + echo "BLAD: kontener uslugi '$svc' nie dziala" >&2 + return 1 + fi + img="$(docker inspect --format '{{.Image}}' "$cid")" + digest="$(docker image inspect --format '{{join .RepoDigests "\n"}}' "$img" \ + | head -1 | sed 's/.*@//')" + if [ -z "$digest" ]; then + echo "BLAD: obraz uslugi '$svc' nie ma RepoDigests (obraz lokalny?)" >&2 + return 1 + fi + printf '%s\n' "$digest" +} +``` + +- [ ] **Step 4: Uruchom testy — mają przejść** + +Run: `bash scripts/test-docker-versions.sh` +Expected: `PASS=9 FAIL=0`, exit 0. + +- [ ] **Step 5: Commit** + +```bash +git add scripts/lib-docker-versions.sh scripts/test-docker-versions.sh +git commit -m "feat(versions): lib digest<->CalVer dla obrazow iplweb + testy (mock curl/docker)" +``` + +--- + +### Task 2: `scripts/zaspawaj-wersje.sh` (TDD) + +**Files:** +- Modify: `scripts/test-docker-versions.sh` (dopisz sekcję testów przed `Podsumowanie`) +- Create: `scripts/zaspawaj-wersje.sh` + +- [ ] **Step 1: Dopisz failing testy** + +W `scripts/test-docker-versions.sh`, PRZED blokiem `# ======================= Podsumowanie =======================`, wstaw: + +```bash +# ===================== Testy zaspawaj-wersje.sh ===================== +echo "" +echo "== zaspawaj-wersje.sh ==" +ZW="$REPO_DIR/scripts/zaspawaj-wersje.sh" + +make_env() { # make_env -> sciezka swiezego katalogu konfiguracyjnego + local dir="$TEST_ROOT/configs-$1" + mkdir -p "$dir" + cat > "$dir/.env" <<'ENVEOF' +DJANGO_BPP_DB_NAME=bpp +DJANGO_BPP_DB_USER=bpp +DJANGO_BPP_DB_PASSWORD=sekret +ENVEOF + printf '%s' "$dir" +} + +# 1. Jawny TAG, poprawny i istniejacy -> wpis w .env, exit 0 +cfg="$(make_env tag-ok)" +rc=0; BPP_CONFIGS_DIR="$cfg" TAG=202606.1386 bash "$ZW" >/dev/null 2>&1 || rc=$? +assert_exit 0 "$rc" "zaspawaj: TAG poprawny -> exit 0" +assert_file_contains "$cfg/.env" "DOCKER_VERSION=202606.1386" "zaspawaj: DOCKER_VERSION zapisany" + +# 2. TAG o zlym formacie -> exit != 0, .env nietkniety +cfg="$(make_env tag-bad)" +rc=0; BPP_CONFIGS_DIR="$cfg" TAG=latest bash "$ZW" >/dev/null 2>&1 || rc=$? +assert_nonzero "$rc" "zaspawaj: TAG=latest odrzucony (nie-CalVer)" +assert_file_not_contains "$cfg/.env" "DOCKER_VERSION" "zaspawaj: .env nietkniety po blednym TAG" + +# 3. TAG nieistniejacy na Hubie -> exit != 0, .env nietkniety +cfg="$(make_env tag-missing)" +rc=0; BPP_CONFIGS_DIR="$cfg" TAG=209912.1 CURL_TAG_EXISTS=0 bash "$ZW" >/dev/null 2>&1 || rc=$? +assert_nonzero "$rc" "zaspawaj: TAG nieistniejacy na Hubie odrzucony" +assert_file_not_contains "$cfg/.env" "DOCKER_VERSION" "zaspawaj: .env nietkniety po nieistniejacym TAG" + +# 4. Bez TAG: wersja rozwiazana z digestu dzialajacego appservera +cfg="$(make_env running)" +rc=0; BPP_CONFIGS_DIR="$cfg" bash "$ZW" >/dev/null 2>&1 || rc=$? +assert_exit 0 "$rc" "zaspawaj: bez TAG -> exit 0 (digest dzialajacego appservera)" +assert_file_contains "$cfg/.env" "DOCKER_VERSION=202606.1386" "zaspawaj: wersja z digestu sha256:aaa" + +# 5. Istniejacy DOCKER_VERSION jest nadpisywany (idempotentne przybicie) +cfg="$(make_env overwrite)" +echo "DOCKER_VERSION=202601.1" >> "$cfg/.env" +rc=0; BPP_CONFIGS_DIR="$cfg" TAG=202606.1386 bash "$ZW" >/dev/null 2>&1 || rc=$? +assert_exit 0 "$rc" "zaspawaj: nadpisanie istniejacej wartosci -> exit 0" +assert_file_contains "$cfg/.env" "DOCKER_VERSION=202606.1386" "zaspawaj: nowa wartosc zapisana" +assert_file_not_contains "$cfg/.env" "DOCKER_VERSION=202601.1" "zaspawaj: stara wartosc usunieta" + +# 6. Appserver nie dziala (i brak TAG) -> exit != 0, .env nietkniety +cfg="$(make_env not-running)" +rc=0; BPP_CONFIGS_DIR="$cfg" MOCK_APPSERVER_RUNNING=0 bash "$ZW" >/dev/null 2>&1 || rc=$? +assert_nonzero "$rc" "zaspawaj: appserver nie dziala -> exit != 0" +assert_file_not_contains "$cfg/.env" "DOCKER_VERSION" "zaspawaj: .env nietkniety gdy appserver nie dziala" +``` + +- [ ] **Step 2: Uruchom testy — nowa sekcja ma polec** + +Run: `bash scripts/test-docker-versions.sh` +Expected: 9 PASS z Taska 1, potem FAIL-e sekcji zaspawaj (brak skryptu), exit != 0. + +- [ ] **Step 3: Zaimplementuj `scripts/zaspawaj-wersje.sh`** + +```bash +#!/usr/bin/env bash +set -euo pipefail +# +# "Zaspawanie" wersji obrazow iplweb: utrwala w $BPP_CONFIGS_DIR/.env +# DOCKER_VERSION=, na ktorym FAKTYCZNIE chodzi produkcyjny +# appserver (celowo nie: na ktory wskazuje lokalny tag :latest - po +# `make pull` bez recreate te dwa moga sie roznic). +# +# Zasieg: DOCKER_VERSION steruje 5 obrazami iplweb (bpp_appserver, +# bpp_authserver, bpp_workerserver, bpp_denorm_queue, bpp_beatserver). +# Pozostale obrazy sa przypiete na sztywno w plikach compose - nie dotykamy. +# +# Uzycie: +# make zaspawaj-wersje # wersja z dzialajacego appservera +# make zaspawaj-wersje TAG=202606.1386 # jawny tag +# +# Nic nie jest restartowane - pin obowiazuje od nastepnej operacji compose. + +REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" +# shellcheck source=scripts/lib-docker-versions.sh +. "$REPO_DIR/scripts/lib-docker-versions.sh" + +# --- BPP_CONFIGS_DIR / ENV_FILE --- +if [ -z "${BPP_CONFIGS_DIR:-}" ] && [ -f "$REPO_DIR/.env" ]; then + BPP_CONFIGS_DIR="$(grep -E '^BPP_CONFIGS_DIR=' "$REPO_DIR/.env" | tail -1 | cut -d= -f2-)" +fi +if [ -z "${BPP_CONFIGS_DIR:-}" ]; then + echo "BLAD: BPP_CONFIGS_DIR nie jest ustawione (brak $REPO_DIR/.env?)" >&2 + exit 1 +fi +export BPP_CONFIGS_DIR +ENV_FILE="$BPP_CONFIGS_DIR/.env" +if [ ! -f "$ENV_FILE" ]; then + echo "BLAD: brak pliku $ENV_FILE" >&2 + exit 1 +fi + +# --- Helpery .env (kopia per-skrypt; konwencja: configure-resources.sh) --- +env_has_var() { grep -q "^${1}=" "$ENV_FILE" 2>/dev/null; } +set_env_var() { + local var_name="$1" value="$2" comment="${3:-}" + if env_has_var "$var_name"; then + local tmp="$ENV_FILE.tmp.$$" + awk -v k="$var_name" -v v="$value" ' + BEGIN { FS=OFS="=" } + $1 == k { print k "=" v; next } + { print } + ' "$ENV_FILE" > "$tmp" && mv "$tmp" "$ENV_FILE" + echo " ~ zaktualizowano ${var_name}=${value}" + else + { + echo "" + if [ -n "$comment" ]; then echo "# $comment"; fi + echo "# Dopisano automatycznie: $(date '+%Y-%m-%d %H:%M:%S')" + echo "${var_name}=${value}" + } >> "$ENV_FILE" + echo " + dodano ${var_name}=${value}" + fi +} + +APPSERVER_REPO="iplweb/bpp_appserver" +cd "$REPO_DIR" + +if [ -n "${TAG:-}" ]; then + # Jawny tag: walidacja formatu + istnienia na Hubie. Dopiero po OBU + # sprawdzeniach dotykamy .env. + if ! printf '%s' "$TAG" | grep -qE "$CALVER_RE"; then + echo "BLAD: TAG='$TAG' nie wyglada na tag CalVer (oczekiwane np. 202606.1386)" >&2 + exit 1 + fi + if ! verify_tag_exists "$APPSERVER_REPO" "$TAG"; then + echo "BLAD: tag '$TAG' nie istnieje w $APPSERVER_REPO na Docker Hubie" >&2 + exit 1 + fi + VERSION="$TAG" + echo "Zaspawuje jawnie podana wersje: $VERSION" +else + echo "Odczytuje wersje z dzialajacego kontenera appserver..." + DIGEST="$(running_repo_digest appserver)" || { + echo "Podpowiedz: uruchom stack (make up) albo podaj wersje jawnie:" >&2 + echo " make zaspawaj-wersje TAG=202606.1386" >&2 + exit 1 + } + VERSION="$(resolve_digest_to_calver "$APPSERVER_REPO" "$DIGEST")" || { + echo "Podpowiedz: obraz nie pochodzi z Docker Huba albo jest starszy niz" >&2 + echo "100 ostatnich tagow. Podaj wersje jawnie: make zaspawaj-wersje TAG=..." >&2 + exit 1 + } + echo "appserver chodzi na ${APPSERVER_REPO}@${DIGEST} = ${VERSION}" + + # Sanity-check (best-effort): czy pozostale uslugi iplweb chodza na tej + # samej wersji? Kazde repo ma wlasne digesty, wiec porownujemy po + # rozwiazanych tagach CalVer. Rozjazd = tylko ostrzezenie. + for pair in authserver:bpp_authserver workerserver:bpp_workerserver \ + denorm-queue:bpp_denorm_queue celerybeat:bpp_beatserver; do + svc="${pair%%:*}"; repo="iplweb/${pair##*:}" + svc_digest="$(running_repo_digest "$svc" 2>/dev/null)" || { + echo " UWAGA: nie moge odczytac obrazu uslugi '$svc' - pomijam" + continue + } + svc_ver="$(resolve_digest_to_calver "$repo" "$svc_digest" 2>/dev/null)" || { + echo " UWAGA: nie moge rozwiazac wersji uslugi '$svc' - pomijam" + continue + } + if [ "$svc_ver" != "$VERSION" ]; then + echo " UWAGA: $svc chodzi na $svc_ver != $VERSION (appserver)." + echo " Spawam wedlug appservera; rozjazd wyrowna nastepne 'make up'." + fi + done +fi + +set_env_var "DOCKER_VERSION" "$VERSION" \ + "Wersja obrazow iplweb/bpp_* (zaspawana przez make zaspawaj-wersje)" + +echo "" +echo "Zaspawano DOCKER_VERSION=${VERSION} w ${ENV_FILE}." +echo "Nic nie zostalo zrestartowane - pin obowiazuje od nastepnej operacji" +echo "docker compose. Aktualizacja na nowsza wersje:" +echo " make zaspawaj-wersje TAG= && make pull && make up" +``` + +(chmod +x) + +- [ ] **Step 4: Uruchom testy — mają przejść** + +Run: `bash scripts/test-docker-versions.sh` +Expected: `PASS=20 FAIL=0`, exit 0. + +- [ ] **Step 5: Commit** + +```bash +git add scripts/zaspawaj-wersje.sh scripts/test-docker-versions.sh +git commit -m "feat(zaspawaj-wersje): pinowanie DOCKER_VERSION do wersji dzialajacego appservera" +``` + +--- + +### Task 3: Targety make + help dla `zaspawaj-wersje` i `test-docker-versions` + +**Files:** +- Modify: `mk/configs.mk` (linia 1: `.PHONY`, koniec pliku: target) +- Modify: `mk/misc.mk` (linia 1: `.PHONY`, koniec pliku: target) +- Modify: `Makefile` (sekcja help "Configuration", po linii `fix-env-quotes`, ok. linii 147) + +- [ ] **Step 1: Dodaj target w `mk/configs.mk`** + +W linii 1 dopisz do `.PHONY`: `zaspawaj-wersje`. Na końcu pliku: + +```makefile +# Przypina DOCKER_VERSION w $(BPP_CONFIGS_DIR)/.env do wersji CalVer, na +# ktorej faktycznie chodzi appserver (lub jawnej: TAG=202606.1386). +# Szczegoly i kontrakt: docs/eksploatacja/komendy.md, CLAUDE.md. +zaspawaj-wersje: + @TAG="$(TAG)" bash scripts/zaspawaj-wersje.sh +``` + +- [ ] **Step 2: Dodaj target w `mk/misc.mk`** + +W linii 1 dopisz do `.PHONY`: `test-docker-versions`. Na końcu pliku: + +```makefile +test-docker-versions: + @bash scripts/test-docker-versions.sh +``` + +- [ ] **Step 3: Dodaj wpisy do `make help` w `Makefile`** + +Po linii `@echo " fix-env-quotes - Auto-strip cudzyslowy z .env (z backupem .bak.)"` dopisz: + +```makefile + @echo " zaspawaj-wersje - Przypnij DOCKER_VERSION do wersji dzialajacego appservera (lub TAG=...)" + @echo " test-docker-versions - Unit-testy logiki wersji obrazow (mock curl/docker, no network)" +``` + +- [ ] **Step 4: Zweryfikuj** + +Run: `make test-docker-versions` +Expected: `PASS=20 FAIL=0`. + +Run: `make help | grep -E "zaspawaj|test-docker"` +Expected: obie linie widoczne. + +Run: `make -n zaspawaj-wersje TAG=202606.1386` +Expected: wypisuje `TAG="202606.1386" bash scripts/zaspawaj-wersje.sh` (dry-run, nic nie wykonuje). + +- [ ] **Step 5: Commit** + +```bash +git add mk/configs.mk mk/misc.mk Makefile +git commit -m "feat(make): targety zaspawaj-wersje + test-docker-versions" +``` + +--- + +### Task 4: `scripts/test-upgrade.sh` (próba generalna + `--clean`) + +**Files:** +- Modify: `scripts/test-docker-versions.sh` (sekcja testów `--clean` przed `Podsumowanie`) +- Create: `scripts/test-upgrade.sh` + +- [ ] **Step 1: Dopisz failing testy `--clean`** + +W `scripts/test-docker-versions.sh`, PRZED blokiem `Podsumowanie`, wstaw: + +```bash +# ===================== Testy test-upgrade.sh (--clean) ===================== +echo "" +echo "== test-upgrade.sh --clean ==" +TU="$REPO_DIR/scripts/test-upgrade.sh" + +: > "$DOCKER_LOG" +rc=0; bash "$TU" --clean >/dev/null 2>&1 || rc=$? +assert_exit 0 "$rc" "test-upgrade --clean: exit 0" +assert_file_contains "$DOCKER_LOG" "rm -f bpp-shadow-dbserver bpp-shadow-redis" "--clean: usuwa kontenery shadow" +assert_file_contains "$DOCKER_LOG" "volume rm -f bpp-shadow-pgdata" "--clean: usuwa wolumen shadow" +assert_file_contains "$DOCKER_LOG" "network rm bpp-shadow" "--clean: usuwa siec shadow" + +rc=0; bash -n "$TU" || rc=$? +assert_exit 0 "$rc" "test-upgrade.sh: poprawna skladnia (bash -n)" +``` + +- [ ] **Step 2: Uruchom testy — sekcja `--clean` ma polec** + +Run: `bash scripts/test-docker-versions.sh` +Expected: dotychczasowe 20 PASS, FAIL-e w sekcji test-upgrade, exit != 0. + +- [ ] **Step 3: Zaimplementuj `scripts/test-upgrade.sh`** + +```bash +#!/usr/bin/env bash +set -euo pipefail +# +# Proba generalna aktualizacji: czy migracje obrazu-kandydata przechodza na +# kopii produkcyjnej bazy? Dziala CALKOWICIE obok produkcji: +# - shadow stack (dbserver+redis) czystym `docker run`, poza projektem +# Compose, na wlasnej sieci bpp-shadow, +# - pull kandydata PO TAGU WERSJI - lokalny tag :latest produkcji +# pozostaje nietkniety, +# - zero zapisu do .env, zero operacji na kontenerach/wolumenach produkcji. +# +# Uzycie: +# make test-upgrade # kandydat = najnowszy CalVer z Huba +# make test-upgrade TAG=202606.1386 # jawny kandydat +# make test-upgrade-clean # sprzatniecie shadow stacka +# +# Wynik: exit 0 = migracje przechodza (shadow posprzatany); +# exit != 0 = blad (shadow ZOSTAJE do inspekcji). + +REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" +# shellcheck source=scripts/lib-docker-versions.sh +. "$REPO_DIR/scripts/lib-docker-versions.sh" + +SHADOW_NET="bpp-shadow" +SHADOW_DB="bpp-shadow-dbserver" +SHADOW_REDIS="bpp-shadow-redis" +SHADOW_VOL="bpp-shadow-pgdata" +PARALLEL_JOBS="${PARALLEL_JOBS:-4}" +# Limity zasobow shadow stacka - przyciete, zeby nie zaglodzic produkcji. +SHADOW_DB_MEM="${SHADOW_DB_MEM:-1g}" +SHADOW_DB_CPUS="${SHADOW_DB_CPUS:-1.0}" +SHADOW_REDIS_MEM="${SHADOW_REDIS_MEM:-256m}" +SHADOW_MIGRATE_MEM="${SHADOW_MIGRATE_MEM:-2g}" + +cleanup_shadow() { + docker rm -f "$SHADOW_DB" "$SHADOW_REDIS" >/dev/null 2>&1 || true + docker volume rm -f "$SHADOW_VOL" >/dev/null 2>&1 || true + docker network rm "$SHADOW_NET" >/dev/null 2>&1 || true +} + +if [ "${1:-}" = "--clean" ]; then + echo "Sprzatam shadow stack ($SHADOW_DB, $SHADOW_REDIS, $SHADOW_VOL, $SHADOW_NET)..." + cleanup_shadow + echo "OK." + exit 0 +fi + +print_inspect_help() { + echo "" >&2 + echo "Shadow stack ZOSTAJE do inspekcji:" >&2 + echo " docker exec -it $SHADOW_DB psql -U \"\$DJANGO_BPP_DB_USER\" -d \"\$DJANGO_BPP_DB_NAME\"" >&2 + echo "Sprzatniecie: make test-upgrade-clean" >&2 +} +trap print_inspect_help ERR + +# --- BPP_CONFIGS_DIR / ENV_FILE --- +if [ -z "${BPP_CONFIGS_DIR:-}" ] && [ -f "$REPO_DIR/.env" ]; then + BPP_CONFIGS_DIR="$(grep -E '^BPP_CONFIGS_DIR=' "$REPO_DIR/.env" | tail -1 | cut -d= -f2-)" +fi +if [ -z "${BPP_CONFIGS_DIR:-}" ]; then + echo "BLAD: BPP_CONFIGS_DIR nie jest ustawione (brak $REPO_DIR/.env?)" >&2 + exit 1 +fi +export BPP_CONFIGS_DIR +ENV_FILE="$BPP_CONFIGS_DIR/.env" +if [ ! -f "$ENV_FILE" ]; then + echo "BLAD: brak pliku $ENV_FILE" >&2 + exit 1 +fi + +# --- Helper .env (kopia per-skrypt; konwencja: init-configs.sh) --- +get_env_var() { + local raw + raw="$(grep -E "^${1}=" "$ENV_FILE" 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" +} + +DB_NAME="$(get_env_var DJANGO_BPP_DB_NAME)" +DB_USER="$(get_env_var DJANGO_BPP_DB_USER)" +DB_PASSWORD="$(get_env_var DJANGO_BPP_DB_PASSWORD)" +if [ -z "$DB_NAME" ] || [ -z "$DB_USER" ] || [ -z "$DB_PASSWORD" ]; then + echo "BLAD: brak DJANGO_BPP_DB_NAME/USER/PASSWORD w $ENV_FILE" >&2 + exit 1 +fi + +# Wersja PG: ta sama logika dwuwarstwowego fallbacku co docker-compose.database.yml. +PG_VERSION="$(get_env_var DJANGO_BPP_POSTGRESQL_VERSION)" +[ -n "$PG_VERSION" ] || PG_VERSION="$(get_env_var DJANGO_BPP_DBSERVER_PG_VERSION)" +[ -n "$PG_VERSION" ] || PG_VERSION="16.13" + +# Katalog backupow: ta sama logika fallbacku co mk/database.mk. +BACKUP_DIR="$(get_env_var DJANGO_BPP_HOST_BACKUP_DIR)" +[ -n "$BACKUP_DIR" ] || BACKUP_DIR="$(get_env_var DJANGO_BPP_BACKUP_DIR)" +[ -n "$BACKUP_DIR" ] || BACKUP_DIR="$(cd "$BPP_CONFIGS_DIR/.." && pwd)/backups" +mkdir -p "$BACKUP_DIR" + +# Wersja redisa: ta sama co produkcyjna (z compose - zero driftu). +REDIS_IMAGE="$(grep -Eo 'redis:[0-9][0-9.]*' "$REPO_DIR/docker-compose.infrastructure.yml" | head -1)" +[ -n "$REDIS_IMAGE" ] || REDIS_IMAGE="redis:8.6.2" + +APPSERVER_REPO="iplweb/bpp_appserver" +cd "$REPO_DIR" + +# --- [1/6] Kandydat --- +echo "=== [1/6] Rozwiazuje obraz-kandydata ===" +if [ -n "${TAG:-}" ]; then + if ! printf '%s' "$TAG" | grep -qE "$CALVER_RE"; then + echo "BLAD: TAG='$TAG' nie wyglada na tag CalVer (np. 202606.1386)" >&2 + exit 1 + fi + CANDIDATE="$TAG" +else + CANDIDATE="$(resolve_latest_calver "$APPSERVER_REPO")" +fi +echo "Kandydat: ${APPSERVER_REPO}:${CANDIDATE}" +# Pull po tagu wersji - lokalny :latest produkcji nietkniety. +docker pull "${APPSERVER_REPO}:${CANDIDATE}" + +# --- [2/6] Kontrola miejsca na dysku --- +echo "=== [2/6] Kontrola miejsca na dysku ===" +if [ "${SKIP_DISK_CHECK:-0}" != "1" ]; then + DB_SIZE_MB="$(docker compose exec -T dbserver psql -U "$DB_USER" -d "$DB_NAME" \ + -tAc "SELECT pg_database_size('$DB_NAME')/1024/1024;" | tr -d '[:space:]')" + NEED_MB=$(( DB_SIZE_MB * 5 / 2 )) # ~2.5x: dump + untar + shadow volume + FREE_BACKUP_MB="$(df -Pm "$BACKUP_DIR" | awk 'NR==2 {print $4}')" + DOCKER_ROOT="$(docker info --format '{{.DockerRootDir}}' 2>/dev/null || echo /var/lib/docker)" + FREE_DOCKER_MB="$(df -Pm "$DOCKER_ROOT" 2>/dev/null | awk 'NR==2 {print $4}')" + [ -n "$FREE_DOCKER_MB" ] || FREE_DOCKER_MB="$FREE_BACKUP_MB" + echo "Baza: ${DB_SIZE_MB} MB; wymagane ~${NEED_MB} MB wolnego miejsca." + if [ "$FREE_BACKUP_MB" -lt "$NEED_MB" ] || [ "$FREE_DOCKER_MB" -lt "$NEED_MB" ]; then + echo "BLAD: za malo miejsca (backup dir: ${FREE_BACKUP_MB} MB, docker root: ${FREE_DOCKER_MB} MB)." >&2 + echo "Wymuszenie pominiecia kontroli: SKIP_DISK_CHECK=1 make test-upgrade" >&2 + exit 1 + fi +else + echo "(pominieta: SKIP_DISK_CHECK=1)" +fi + +# --- [3/6] Backup produkcyjnej bazy --- +echo "=== [3/6] Backup produkcyjnej bazy (make db-backup) ===" +make -C "$REPO_DIR" db-backup +BACKUP_TAR_PATH="$(ls -t "$BACKUP_DIR"/db-backup-*.tar.gz 2>/dev/null | head -1)" +if [ -z "$BACKUP_TAR_PATH" ]; then + echo "BLAD: nie znalazlem swiezego dumpa w $BACKUP_DIR" >&2 + exit 1 +fi +BACKUP_TAR="$(basename "$BACKUP_TAR_PATH")" +BACKUP_DIRNAME="${BACKUP_TAR%.tar.gz}" +echo "Dump: $BACKUP_TAR_PATH" + +# --- [4/6] Shadow stack --- +echo "=== [4/6] Stawiam shadow stack (siec $SHADOW_NET) ===" +cleanup_shadow # zombie z poprzedniego przebiegu +docker network create "$SHADOW_NET" >/dev/null +docker volume create "$SHADOW_VOL" >/dev/null +docker run -d --name "$SHADOW_DB" --network "$SHADOW_NET" \ + -e POSTGRES_DB="$DB_NAME" \ + -e POSTGRES_USER="$DB_USER" \ + -e POSTGRES_PASSWORD="$DB_PASSWORD" \ + -v "$SHADOW_VOL":/var/lib/postgresql/data \ + -v "$BACKUP_DIR":/backup:ro \ + --memory "$SHADOW_DB_MEM" --cpus "$SHADOW_DB_CPUS" \ + "iplweb/bpp_dbserver:psql-${PG_VERSION}" >/dev/null +docker run -d --name "$SHADOW_REDIS" --network "$SHADOW_NET" \ + --memory "$SHADOW_REDIS_MEM" \ + "$REDIS_IMAGE" >/dev/null + +echo "Czekam na gotowosc shadow-postgresa..." +for i in $(seq 1 60); do + if docker exec "$SHADOW_DB" pg_isready -U "$DB_USER" -d "$DB_NAME" >/dev/null 2>&1; then + break + fi + if [ "$i" -eq 60 ]; then + echo "BLAD: shadow-postgres nie wstal w 120 s" >&2 + exit 1 + fi + sleep 2 +done + +# --- [5/6] Restore dumpa do shadow-bazy --- +echo "=== [5/6] Restore dumpa do shadow-bazy (pg_restore -j $PARALLEL_JOBS) ===" +docker exec "$SHADOW_DB" mkdir -p /tmp/restore +docker exec "$SHADOW_DB" tar xzf "/backup/$BACKUP_TAR" -C /tmp/restore +docker exec "$SHADOW_DB" pg_restore -Fd -j "$PARALLEL_JOBS" --no-owner \ + -U "$DB_USER" -d "$DB_NAME" "/tmp/restore/$BACKUP_DIRNAME" + +# --- [6/6] Migracja obrazem-kandydatem --- +echo "=== [6/6] manage.py migrate obrazem ${APPSERVER_REPO}:${CANDIDATE} ===" +# Entrypoint nadpisany: zadnych faz startowych (staticfiles, gunicorn) - +# wylacznie migracja. --env-file daje komplet zmiennych jak w produkcji, +# -e nadpisuje hosty na shadow. +set +e +docker run --rm --network "$SHADOW_NET" \ + --env-file "$ENV_FILE" \ + -e DJANGO_BPP_DB_HOST="$SHADOW_DB" \ + -e DJANGO_BPP_DB_PORT=5432 \ + -e DJANGO_BPP_REDIS_HOST="$SHADOW_REDIS" \ + --memory "$SHADOW_MIGRATE_MEM" \ + --entrypoint python \ + "${APPSERVER_REPO}:${CANDIDATE}" src/manage.py migrate --noinput +MIGRATE_RC=$? +set -e + +if [ "$MIGRATE_RC" -eq 0 ]; then + trap - ERR + echo "" + echo "=== OK: migracje ${CANDIDATE} przechodza na kopii produkcyjnej bazy ===" + echo "Sprzatam shadow stack..." + cleanup_shadow + echo "Gotowe. Produkcja przez caly czas byla nietknieta." + exit 0 +else + echo "" >&2 + echo "=== BLAD: migracja ${CANDIDATE} NIE przeszla (exit=$MIGRATE_RC) ===" >&2 + echo "Shadow stack ZOSTAJE do inspekcji:" >&2 + echo " docker exec -it $SHADOW_DB psql -U $DB_USER -d $DB_NAME" >&2 + echo "Ponowna proba migracji (po obejrzeniu):" >&2 + echo " TAG=$CANDIDATE make test-upgrade" >&2 + echo "Sprzatniecie: make test-upgrade-clean" >&2 + exit 1 +fi +``` + +(chmod +x) + +- [ ] **Step 4: Uruchom testy — mają przejść** + +Run: `bash scripts/test-docker-versions.sh` +Expected: `PASS=25 FAIL=0`, exit 0. + +- [ ] **Step 5: Commit** + +```bash +git add scripts/test-upgrade.sh scripts/test-docker-versions.sh +git commit -m "feat(test-upgrade): proba generalna migracji kandydata na shadow stacku" +``` + +--- + +### Task 5: Targety make + help dla `test-upgrade` + +**Files:** +- Modify: `mk/deployment.mk` (linia 1: `.PHONY`, koniec pliku: targety) +- Modify: `Makefile` (sekcja help "Deployment", po linii `wait`, ok. linii 94) + +- [ ] **Step 1: Dodaj targety w `mk/deployment.mk`** + +W linii 1 dopisz do `.PHONY`: `test-upgrade test-upgrade-clean`. Na końcu pliku (przed linią `run: ...` lub po niej — bez znaczenia, byle nie wewnątrz innego targetu): + +```makefile +# Proba generalna aktualizacji: backup -> shadow stack (dbserver+redis poza +# projektem Compose) -> restore -> migrate obrazem-kandydatem. Produkcja +# (kontenery, wolumeny, lokalny tag :latest, .env) pozostaje nietknieta. +# Uwaga: NIE mylic z test-upgrade-postgres (unit-testy upgrade'u PG). +test-upgrade: validate-env-quotes + @TAG="$(TAG)" bash scripts/test-upgrade.sh + +test-upgrade-clean: + @bash scripts/test-upgrade.sh --clean +``` + +- [ ] **Step 2: Dodaj wpisy do `make help` w `Makefile`** + +Po linii `@echo " wait - Wait for Docker build, then pull and restart"` dopisz: + +```makefile + @echo " test-upgrade - Proba generalna: migracje kandydata na kopii bazy (TAG=...)" + @echo " test-upgrade-clean - Sprzatniecie shadow stacka po nieudanym test-upgrade" +``` + +- [ ] **Step 3: Zweryfikuj** + +Run: `make help | grep test-upgrade` +Expected: obie nowe linie (oraz istniejące targety bez zmian). + +Run: `make -n test-upgrade-clean` +Expected: `bash scripts/test-upgrade.sh --clean`. + +Run: `make test-upgrade-clean` +Expected: "Sprzatam shadow stack..." + "OK." (idempotentne — niczego nie ma do sprzątnięcia; wymaga działającego dockera). + +- [ ] **Step 4: Commit** + +```bash +git add mk/deployment.mk Makefile +git commit -m "feat(make): targety test-upgrade + test-upgrade-clean" +``` + +--- + +### Task 6: Dokumentacja (docs-sync) + +**Files:** +- Modify: `docs/eksploatacja/komendy.md` (nowa sekcja na końcu pliku) +- Modify: `CLAUDE.md` (nowa podsekcja po "### PostgreSQL version vars") + +- [ ] **Step 1: Sekcja w `docs/eksploatacja/komendy.md`** + +Na końcu pliku dopisz: + +```markdown +## Aktualizacje i wersje obrazów + +### `make zaspawaj-wersje` — pinowanie wersji obrazów iplweb + +Domyślnie obrazy `iplweb/bpp_*` jadą na ruchomym tagu `latest` — każdy +`make pull` może podmienić wersję. `zaspawaj-wersje` utrwala w +`$BPP_CONFIGS_DIR/.env` zmienną `DOCKER_VERSION=` odpowiadającą +wersji, na której **faktycznie chodzi** kontener `appserver` (nie tej, na +którą wskazuje lokalny tag `latest` — po `make pull` bez recreate te dwie +mogą się różnić). + +```bash +make zaspawaj-wersje # wersja z działającego appservera +make zaspawaj-wersje TAG=202606.1386 # jawny tag +``` + +Po zaspawaniu `restart`, awaryjny recreate i nocne restarty Ofelii trzymają +się przypiętej wersji. Aktualizacja na nowszą wersję wymaga jawnej decyzji: + +```bash +make zaspawaj-wersje TAG= && make pull && make up +``` + +Zmienna obejmuje 5 obrazów iplweb (`bpp_appserver`, `bpp_authserver`, +`bpp_workerserver`, `bpp_denorm_queue`, `bpp_beatserver`). Pozostałe obrazy +(nginx, redis, grafana, …) są przypięte na sztywno w plikach compose; +PostgreSQL ma własną `DJANGO_BPP_POSTGRESQL_VERSION`. + +### `make test-upgrade` — próba generalna migracji + +Sprawdza, czy migracje bazodanowe obrazu-kandydata przechodzą na **kopii +produkcyjnej bazy**, zanim czegokolwiek dotkniesz na produkcji: + +1. pobiera obraz-kandydat **po tagu wersji** (lokalny `latest` nietknięty), +2. robi świeży `make db-backup` (błąd backupu przerywa całość), +3. stawia shadow stack (`bpp-shadow-dbserver` + `bpp-shadow-redis`) na + osobnej sieci, poza projektem Compose, z przyciętymi limitami zasobów, +4. restoruje dump do shadow-bazy, +5. uruchamia `manage.py migrate` obrazem-kandydatem (entrypoint nadpisany — + nic poza migracją się nie uruchamia). + +```bash +make test-upgrade # kandydat = najnowszy tag CalVer z Docker Huba +make test-upgrade TAG=202606.1386 # jawny kandydat +``` + +**Sukces** → shadow stack jest sprzątany, exit 0. **Porażka** → shadow stack +zostaje do inspekcji (`docker exec -it bpp-shadow-dbserver psql ...`); +sprzątasz przez `make test-upgrade-clean`. + +Wymagania: wolne miejsce na dysku ≈ 2,5× rozmiar bazy (kontrolowane przed +startem; wymuszenie pominięcia kontroli: `SKIP_DISK_CHECK=1`). Próba +obciąża CPU/IO hosta na czas dump+restore — na małych hostach uruchamiaj +poza godzinami szczytu. + +Typowy przepływ bezpiecznej aktualizacji na zaspawanym hoście: + +```bash +make test-upgrade # migracje kandydata przechodzą? +make zaspawaj-wersje TAG= # przypnij nową wersję +make pull && make up # właściwy deploy (health-gate --wait) +``` +``` + +- [ ] **Step 2: Podsekcja w `CLAUDE.md`** + +Po podsekcji `### PostgreSQL version vars` (przed `## Critical Deployment Patterns`) dopisz: + +```markdown +### Image version pinning (`DOCKER_VERSION`) and upgrade rehearsal + +`DOCKER_VERSION` pins the 5 `iplweb/bpp_*` images (default `latest` — compose +fallback `${DOCKER_VERSION:-latest}` must stay for backwards compat). +`make zaspawaj-wersje` welds the version **actually running in the appserver +container** (not the local `latest` tag) into `.env` via the stable +`set_env_var` helper; updating a pinned host requires an explicit +`make zaspawaj-wersje TAG=`. `make test-upgrade` is the migration +rehearsal: fresh `db-backup` → shadow stack (`bpp-shadow-*`, plain +`docker run` outside the Compose project) → `pg_restore` → candidate-image +`manage.py migrate` with overridden entrypoint. It must never touch +production containers, volumes, the local `latest` tag, or `.env`. Candidate +images are pulled **by version tag**, never via `:latest`. Shared +digest↔CalVer logic lives in `scripts/lib-docker-versions.sh` +(tests: `make test-docker-versions`). Detail: `docs/eksploatacja/komendy.md`. +``` + +- [ ] **Step 3: Zbuduj dokumentację** + +Run: `mkdocs build --strict` +Expected: build bez warningów/błędów (komenda dostępna w repo; jeśli brak — `pip install mkdocs-material` zgodnie z docs/rozwoj). + +- [ ] **Step 4: Commit** + +```bash +git add docs/eksploatacja/komendy.md CLAUDE.md +git commit -m "docs: zaspawaj-wersje + test-upgrade (komendy.md, CLAUDE.md)" +``` + +--- + +### Task 7: Weryfikacja końcowa + +- [ ] **Step 1: Komplet testów jednostkowych repo** + +Run: `make test-docker-versions && make test-validate-env-quotes && make test-letsencrypt` +Expected: wszystkie PASS, exit 0. + +- [ ] **Step 2: Smoke targetów (dry-run)** + +Run: `make -n test-upgrade && make -n test-upgrade-clean && make -n zaspawaj-wersje` +Expected: poprawne komendy, bez błędów make (np. brakujących zależności targetów). + +- [ ] **Step 3: Manualny test na hoście stagingowym (poza CI — checklist dla operatora)** + +Na hoście z działającym stackiem: + +1. `make zaspawaj-wersje` → `DOCKER_VERSION` w `.env` odpowiada wersji działającego appservera; `docker compose config | grep image:` pokazuje przypięte tagi. +2. `make test-upgrade` → pełny przebieg, sukces, shadow posprzątany (`docker ps -a | grep bpp-shadow` puste). +3. Symulowana porażka: `make test-upgrade TAG=` lub przerwanie restore — shadow zostaje, komunikat o inspekcji, `make test-upgrade-clean` sprząta. +4. Uwaga na wynik `pg_restore`: jeśli na realnej bazie zwróci niezerowy kod przez nieszkodliwe błędy (np. `COMMENT ON EXTENSION`), zanotować i rozważyć dopuszczalną listę — NIE maskować ślepo (`|| true` zakazane). + +- [ ] **Step 4: Commit końcowy (jeśli były poprawki)** + +```bash +git add -A scripts/ mk/ Makefile docs/ CLAUDE.md +git commit -m "fix(test-upgrade): poprawki po weryfikacji" +``` + +--- + +## Self-Review (wykonany) + +- **Spec coverage:** lib (Task 1), zaspawaj-wersje + tryby błędu (Task 2–3), test-upgrade + clean + gwarancje nienaruszalności + kontrola dysku (Task 4–5), docs + CLAUDE.md + mkdocs strict (Task 6), testy w konwencji repo + manualny staging (Task 1–4, 7). Kompatybilność wsteczna: żadnych nowych wymaganych zmiennych; `${DOCKER_VERSION:-latest}` nietknięte. +- **Placeholder scan:** brak TBD/TODO; każdy krok ma pełny kod lub dokładną komendę. +- **Type consistency:** nazwy funkcji lib (`resolve_latest_calver`, `resolve_digest_to_calver`, `verify_tag_exists`, `running_repo_digest`) i zmiennych shadow (`bpp-shadow-*`) spójne między taskami; liczby PASS narastają 9 → 20 → 25. From cfe1614ab9bd28817c0c1c149ff72b1cda327ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Fri, 12 Jun 2026 14:58:21 +0200 Subject: [PATCH 03/11] feat(versions): lib digest<->CalVer dla obrazow iplweb + testy (mock curl/docker) Co-Authored-By: Claude Fable 5 --- scripts/lib-docker-versions.sh | 83 +++++++++++++++++ scripts/test-docker-versions.sh | 156 ++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100755 scripts/lib-docker-versions.sh create mode 100755 scripts/test-docker-versions.sh diff --git a/scripts/lib-docker-versions.sh b/scripts/lib-docker-versions.sh new file mode 100755 index 0000000..fe425f2 --- /dev/null +++ b/scripts/lib-docker-versions.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# Wspolne funkcje mapowania digest <-> tag CalVer dla obrazow iplweb/* na +# Docker Hubie. Source'owana przez scripts/test-upgrade.sh i +# scripts/zaspawaj-wersje.sh - bez side-effectow przy source. +# Zaleznosci: curl, jq, docker (tylko running_repo_digest). +# +# Tagi CalVer obrazow BPP: YYYYMM.NNNN (np. 202606.1386). `latest` na Hubie +# wskazuje ten sam digest co najnowszy tag CalVer. + +# Wzorzec tagu CalVer (BSD/GNU `grep -E` compatible). +CALVER_RE='^[0-9]{6}\.[0-9]+$' + +# Endpoint API - nadpisywalny w testach. +HUB_API="${HUB_API:-https://hub.docker.com/v2}" + +# _hub_tags_json -- surowy JSON pierwszych 100 tagow repozytorium. +_hub_tags_json() { + curl -fsS "$HUB_API/repositories/$1/tags?page_size=100" +} + +# resolve_latest_calver +# stdout: najnowszy (numerycznie) tag CalVer; exit 1 gdy brak / blad sieci. +resolve_latest_calver() { + local repo="$1" tag + tag="$(_hub_tags_json "$repo" \ + | jq -r '.results[].name' \ + | grep -E "$CALVER_RE" \ + | sort -t. -k1,1n -k2,2n \ + | tail -1)" || true + if [ -z "$tag" ]; then + echo "BLAD: nie znaleziono tagu CalVer dla $repo (siec? API Huba?)" >&2 + return 1 + fi + printf '%s\n' "$tag" +} + +# resolve_digest_to_calver +# stdout: tag CalVer o tym digescie (manifest-list LUB per-arch z .images[]); +# exit 1 gdy nie znaleziono. +resolve_digest_to_calver() { + local repo="$1" digest="$2" tag + tag="$(_hub_tags_json "$repo" \ + | jq -r --arg d "$digest" \ + '.results[] + | select(((.digest // "") == $d) + or (([.images[]?.digest // empty] | index($d)) != null)) + | .name' \ + | grep -E "$CALVER_RE" \ + | head -1)" || true + if [ -z "$tag" ]; then + echo "BLAD: digest $digest nie odpowiada zadnemu tagowi CalVer w $repo" >&2 + return 1 + fi + printf '%s\n' "$tag" +} + +# verify_tag_exists -- exit 0 gdy tag istnieje na Hubie. +verify_tag_exists() { + curl -fsS "$HUB_API/repositories/$1/tags/$2" >/dev/null 2>&1 +} + +# running_repo_digest +# stdout: digest (sha256:...) obrazu, na ktorym CHODZI kontener uslugi - +# celowo nie z lokalnego tagu :latest (po `make pull` bez recreate lokalny +# tag moze juz wskazywac nowszy obraz niz dzialajacy kontener). +# Wymaga CWD = katalog repo (docker compose). exit 1 gdy kontener nie dziala +# albo obraz nie ma RepoDigests (np. budowany lokalnie). +running_repo_digest() { + local svc="$1" cid img digest + cid="$(docker compose ps -q "$svc" 2>/dev/null | head -1)" + if [ -z "$cid" ]; then + echo "BLAD: kontener uslugi '$svc' nie dziala" >&2 + return 1 + fi + img="$(docker inspect --format '{{.Image}}' "$cid")" + digest="$(docker image inspect --format '{{join .RepoDigests "\n"}}' "$img" \ + | head -1 | sed 's/.*@//')" + if [ -z "$digest" ]; then + echo "BLAD: obraz uslugi '$svc' nie ma RepoDigests (obraz lokalny?)" >&2 + return 1 + fi + printf '%s\n' "$digest" +} diff --git a/scripts/test-docker-versions.sh b/scripts/test-docker-versions.sh new file mode 100755 index 0000000..b5f2b2e --- /dev/null +++ b/scripts/test-docker-versions.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# +# Testy scripts/lib-docker-versions.sh, scripts/zaspawaj-wersje.sh oraz +# scripts/test-upgrade.sh (sciezka --clean). +# +# Bez sieci i bez Docker daemona: `curl` i `docker` sa mockowane +# stub-skryptami w PATH (konwencja: scripts/test-letsencrypt.sh). +# +# Uruchomienie: `make test-docker-versions` lub +# `bash scripts/test-docker-versions.sh` + +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" +LIB="$REPO_DIR/scripts/lib-docker-versions.sh" + +TEST_ROOT="$(mktemp -d -t bpp-docker-versions-test-XXXXXX)" +MOCK_BIN="$TEST_ROOT/mock-bin" +CURL_LOG="$TEST_ROOT/curl-calls.log" +DOCKER_LOG="$TEST_ROOT/docker-calls.log" +FIXTURES="$TEST_ROOT/fixtures" +mkdir -p "$MOCK_BIN" "$FIXTURES" + +# shellcheck disable=SC2317 # wywolywane przez trap +cleanup() { rm -rf "$TEST_ROOT"; } +trap cleanup EXIT + +green() { printf "\033[32m%s\033[0m\n" "$*"; } +red() { printf "\033[31m%s\033[0m\n" "$*"; } +PASS=0; FAIL=0 +pass() { green " PASS: $1"; PASS=$((PASS + 1)); } +fail() { red " FAIL: $1"; FAIL=$((FAIL + 1)); } + +assert_eq() { + local expected="$1" actual="$2" name="$3" + if [ "$expected" = "$actual" ]; then pass "$name"; else + fail "$name (oczekiwane '$expected', otrzymano '$actual')"; fi +} +assert_exit() { + local expected="$1" actual="$2" name="$3" + if [ "$expected" = "$actual" ]; then pass "$name"; else + fail "$name (oczekiwany exit=$expected, otrzymany exit=$actual)"; fi +} +assert_nonzero() { + local actual="$1" name="$2" + if [ "$actual" != "0" ]; then pass "$name"; else + fail "$name (oczekiwany exit != 0, otrzymany 0)"; fi +} +assert_file_contains() { + local file="$1" needle="$2" name="$3" + if grep -qF -- "$needle" "$file" 2>/dev/null; then pass "$name"; else + fail "$name (brak '$needle' w $file)"; fi +} +assert_file_not_contains() { + local file="$1" needle="$2" name="$3" + if grep -qF -- "$needle" "$file" 2>/dev/null; then + fail "$name ('$needle' obecne w $file)"; else pass "$name"; fi +} + +# --- Mock curl --- +# Ostatni argument wywolania = URL. +# .../tags?page_size=... -> cat $CURL_FIXTURE (lista tagow) +# .../tags/ -> exit 0 gdy CURL_TAG_EXISTS=1 (domyslnie), inaczej 22 +cat > "$MOCK_BIN/curl" <> "$CURL_LOG" +case "\$last" in + *"/tags?page_size="*) + if [ -n "\${CURL_FIXTURE:-}" ]; then cat "\$CURL_FIXTURE"; else exit 22; fi ;; + *"/tags/"*) + [ "\${CURL_TAG_EXISTS:-1}" = "1" ] || exit 22 ;; + *) exit 22 ;; +esac +EOF +chmod +x "$MOCK_BIN/curl" + +# --- Mock docker --- +# compose ps -q appserver -> cid (pusty gdy MOCK_APPSERVER_RUNNING=0) +# compose ps -q -> cid-other +# inspect --format {{.Image}} cid-* -> img-123 +# image inspect --format ... img-123 -> repo digest (MOCK_RUNNING_DIGEST) +cat > "$MOCK_BIN/docker" <> "$DOCKER_LOG" +case "\$*" in + "compose ps -q appserver") + if [ "\${MOCK_APPSERVER_RUNNING:-1}" = "1" ]; then echo "cid-app"; fi ;; + "compose ps -q "*) + echo "cid-other" ;; + "inspect --format {{.Image}} cid-"*) + echo "img-123" ;; + "image inspect --format "*) + echo "iplweb/bpp_appserver@\${MOCK_RUNNING_DIGEST:-sha256:aaa}" ;; + *) exit 0 ;; +esac +EOF +chmod +x "$MOCK_BIN/docker" + +export PATH="$MOCK_BIN:$PATH" + +# --- Fixtures API Docker Huba --- +cat > "$FIXTURES/tags.json" <<'EOF' +{"results":[ + {"name":"latest","digest":"sha256:aaa","images":[{"digest":"sha256:arm64aaa"},{"digest":"sha256:amd64aaa"}]}, + {"name":"feature-multi-hosted-config","digest":"sha256:fff","images":[]}, + {"name":"sha-56127ac","digest":"sha256:aaa","images":[]}, + {"name":"202606.1386","digest":"sha256:aaa","images":[{"digest":"sha256:arm64aaa"},{"digest":"sha256:amd64aaa"}]}, + {"name":"202606.999","digest":"sha256:bbb","images":[{"digest":"sha256:armbbb"}]}, + {"name":"202605.1300","digest":"sha256:ccc","images":[]} +]} +EOF +cat > "$FIXTURES/no-calver.json" <<'EOF' +{"results":[{"name":"latest","digest":"sha256:aaa","images":[]}]} +EOF + +# ===================== Testy lib-docker-versions.sh ===================== +echo "== lib-docker-versions.sh ==" +# shellcheck source=/dev/null +. "$LIB" + +export CURL_FIXTURE="$FIXTURES/tags.json" + +out="$(resolve_latest_calver iplweb/bpp_appserver)" +assert_eq "202606.1386" "$out" "resolve_latest_calver: najnowszy numerycznie (1386 > 999)" + +out="$(resolve_digest_to_calver iplweb/bpp_appserver sha256:aaa)" +assert_eq "202606.1386" "$out" "resolve_digest_to_calver: digest manifest-list" + +out="$(resolve_digest_to_calver iplweb/bpp_appserver sha256:armbbb)" +assert_eq "202606.999" "$out" "resolve_digest_to_calver: digest per-arch (images[])" + +rc=0; resolve_digest_to_calver iplweb/bpp_appserver sha256:zzz >/dev/null 2>&1 || rc=$? +assert_nonzero "$rc" "resolve_digest_to_calver: nieznany digest -> exit != 0" + +CURL_FIXTURE="$FIXTURES/no-calver.json" +rc=0; resolve_latest_calver iplweb/bpp_appserver >/dev/null 2>&1 || rc=$? +assert_nonzero "$rc" "resolve_latest_calver: brak tagow CalVer -> exit != 0" +CURL_FIXTURE="$FIXTURES/tags.json" + +rc=0; verify_tag_exists iplweb/bpp_appserver 202606.1386 || rc=$? +assert_exit 0 "$rc" "verify_tag_exists: istniejacy tag -> exit 0" + +rc=0; CURL_TAG_EXISTS=0 verify_tag_exists iplweb/bpp_appserver 999999.1 || rc=$? +assert_nonzero "$rc" "verify_tag_exists: brak tagu -> exit != 0" + +out="$(running_repo_digest appserver)" +assert_eq "sha256:aaa" "$out" "running_repo_digest: digest z dzialajacego kontenera" + +rc=0; MOCK_APPSERVER_RUNNING=0 running_repo_digest appserver >/dev/null 2>&1 || rc=$? +assert_nonzero "$rc" "running_repo_digest: kontener nie dziala -> exit != 0" + +# ======================= Podsumowanie ======================= +echo "" +echo "Wynik: PASS=$PASS FAIL=$FAIL" +[ "$FAIL" -eq 0 ] From 500a715bd536b30642a15e178fd77f82656a6093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Fri, 12 Jun 2026 15:01:56 +0200 Subject: [PATCH 04/11] feat(zaspawaj-wersje): pinowanie DOCKER_VERSION do wersji dzialajacego appservera Co-Authored-By: Claude Fable 5 --- scripts/test-docker-versions.sh | 54 +++++++++++++++ scripts/zaspawaj-wersje.sh | 119 ++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100755 scripts/zaspawaj-wersje.sh diff --git a/scripts/test-docker-versions.sh b/scripts/test-docker-versions.sh index b5f2b2e..3029621 100755 --- a/scripts/test-docker-versions.sh +++ b/scripts/test-docker-versions.sh @@ -150,6 +150,60 @@ assert_eq "sha256:aaa" "$out" "running_repo_digest: digest z dzialajacego konten rc=0; MOCK_APPSERVER_RUNNING=0 running_repo_digest appserver >/dev/null 2>&1 || rc=$? assert_nonzero "$rc" "running_repo_digest: kontener nie dziala -> exit != 0" +# ===================== Testy zaspawaj-wersje.sh ===================== +echo "" +echo "== zaspawaj-wersje.sh ==" +ZW="$REPO_DIR/scripts/zaspawaj-wersje.sh" + +make_env() { # make_env -> sciezka swiezego katalogu konfiguracyjnego + local dir="$TEST_ROOT/configs-$1" + mkdir -p "$dir" + cat > "$dir/.env" <<'ENVEOF' +DJANGO_BPP_DB_NAME=bpp +DJANGO_BPP_DB_USER=bpp +DJANGO_BPP_DB_PASSWORD=sekret +ENVEOF + printf '%s' "$dir" +} + +# 1. Jawny TAG, poprawny i istniejacy -> wpis w .env, exit 0 +cfg="$(make_env tag-ok)" +rc=0; BPP_CONFIGS_DIR="$cfg" TAG=202606.1386 bash "$ZW" >/dev/null 2>&1 || rc=$? +assert_exit 0 "$rc" "zaspawaj: TAG poprawny -> exit 0" +assert_file_contains "$cfg/.env" "DOCKER_VERSION=202606.1386" "zaspawaj: DOCKER_VERSION zapisany" + +# 2. TAG o zlym formacie -> exit != 0, .env nietkniety +cfg="$(make_env tag-bad)" +rc=0; BPP_CONFIGS_DIR="$cfg" TAG=latest bash "$ZW" >/dev/null 2>&1 || rc=$? +assert_nonzero "$rc" "zaspawaj: TAG=latest odrzucony (nie-CalVer)" +assert_file_not_contains "$cfg/.env" "DOCKER_VERSION" "zaspawaj: .env nietkniety po blednym TAG" + +# 3. TAG nieistniejacy na Hubie -> exit != 0, .env nietkniety +cfg="$(make_env tag-missing)" +rc=0; BPP_CONFIGS_DIR="$cfg" TAG=209912.1 CURL_TAG_EXISTS=0 bash "$ZW" >/dev/null 2>&1 || rc=$? +assert_nonzero "$rc" "zaspawaj: TAG nieistniejacy na Hubie odrzucony" +assert_file_not_contains "$cfg/.env" "DOCKER_VERSION" "zaspawaj: .env nietkniety po nieistniejacym TAG" + +# 4. Bez TAG: wersja rozwiazana z digestu dzialajacego appservera +cfg="$(make_env running)" +rc=0; BPP_CONFIGS_DIR="$cfg" bash "$ZW" >/dev/null 2>&1 || rc=$? +assert_exit 0 "$rc" "zaspawaj: bez TAG -> exit 0 (digest dzialajacego appservera)" +assert_file_contains "$cfg/.env" "DOCKER_VERSION=202606.1386" "zaspawaj: wersja z digestu sha256:aaa" + +# 5. Istniejacy DOCKER_VERSION jest nadpisywany (idempotentne przybicie) +cfg="$(make_env overwrite)" +echo "DOCKER_VERSION=202601.1" >> "$cfg/.env" +rc=0; BPP_CONFIGS_DIR="$cfg" TAG=202606.1386 bash "$ZW" >/dev/null 2>&1 || rc=$? +assert_exit 0 "$rc" "zaspawaj: nadpisanie istniejacej wartosci -> exit 0" +assert_file_contains "$cfg/.env" "DOCKER_VERSION=202606.1386" "zaspawaj: nowa wartosc zapisana" +assert_file_not_contains "$cfg/.env" "DOCKER_VERSION=202601.1" "zaspawaj: stara wartosc usunieta" + +# 6. Appserver nie dziala (i brak TAG) -> exit != 0, .env nietkniety +cfg="$(make_env not-running)" +rc=0; BPP_CONFIGS_DIR="$cfg" MOCK_APPSERVER_RUNNING=0 bash "$ZW" >/dev/null 2>&1 || rc=$? +assert_nonzero "$rc" "zaspawaj: appserver nie dziala -> exit != 0" +assert_file_not_contains "$cfg/.env" "DOCKER_VERSION" "zaspawaj: .env nietkniety gdy appserver nie dziala" + # ======================= Podsumowanie ======================= echo "" echo "Wynik: PASS=$PASS FAIL=$FAIL" diff --git a/scripts/zaspawaj-wersje.sh b/scripts/zaspawaj-wersje.sh new file mode 100755 index 0000000..9ad7c44 --- /dev/null +++ b/scripts/zaspawaj-wersje.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +set -euo pipefail +# +# "Zaspawanie" wersji obrazow iplweb: utrwala w $BPP_CONFIGS_DIR/.env +# DOCKER_VERSION=, na ktorym FAKTYCZNIE chodzi produkcyjny +# appserver (celowo nie: na ktory wskazuje lokalny tag :latest - po +# `make pull` bez recreate te dwa moga sie roznic). +# +# Zasieg: DOCKER_VERSION steruje 5 obrazami iplweb (bpp_appserver, +# bpp_authserver, bpp_workerserver, bpp_denorm_queue, bpp_beatserver). +# Pozostale obrazy sa przypiete na sztywno w plikach compose - nie dotykamy. +# +# Uzycie: +# make zaspawaj-wersje # wersja z dzialajacego appservera +# make zaspawaj-wersje TAG=202606.1386 # jawny tag +# +# Nic nie jest restartowane - pin obowiazuje od nastepnej operacji compose. + +REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" +# shellcheck source=scripts/lib-docker-versions.sh +. "$REPO_DIR/scripts/lib-docker-versions.sh" + +# --- BPP_CONFIGS_DIR / ENV_FILE --- +if [ -z "${BPP_CONFIGS_DIR:-}" ] && [ -f "$REPO_DIR/.env" ]; then + BPP_CONFIGS_DIR="$(grep -E '^BPP_CONFIGS_DIR=' "$REPO_DIR/.env" | tail -1 | cut -d= -f2-)" +fi +if [ -z "${BPP_CONFIGS_DIR:-}" ]; then + echo "BLAD: BPP_CONFIGS_DIR nie jest ustawione (brak $REPO_DIR/.env?)" >&2 + exit 1 +fi +export BPP_CONFIGS_DIR +ENV_FILE="$BPP_CONFIGS_DIR/.env" +if [ ! -f "$ENV_FILE" ]; then + echo "BLAD: brak pliku $ENV_FILE" >&2 + exit 1 +fi + +# --- Helpery .env (kopia per-skrypt; konwencja: configure-resources.sh) --- +env_has_var() { grep -q "^${1}=" "$ENV_FILE" 2>/dev/null; } +set_env_var() { + local var_name="$1" value="$2" comment="${3:-}" + if env_has_var "$var_name"; then + local tmp="$ENV_FILE.tmp.$$" + awk -v k="$var_name" -v v="$value" ' + BEGIN { FS=OFS="=" } + $1 == k { print k "=" v; next } + { print } + ' "$ENV_FILE" > "$tmp" && mv "$tmp" "$ENV_FILE" + echo " ~ zaktualizowano ${var_name}=${value}" + else + { + echo "" + if [ -n "$comment" ]; then echo "# $comment"; fi + echo "# Dopisano automatycznie: $(date '+%Y-%m-%d %H:%M:%S')" + echo "${var_name}=${value}" + } >> "$ENV_FILE" + echo " + dodano ${var_name}=${value}" + fi +} + +APPSERVER_REPO="iplweb/bpp_appserver" +cd "$REPO_DIR" + +if [ -n "${TAG:-}" ]; then + # Jawny tag: walidacja formatu + istnienia na Hubie. Dopiero po OBU + # sprawdzeniach dotykamy .env. + if ! printf '%s' "$TAG" | grep -qE "$CALVER_RE"; then + echo "BLAD: TAG='$TAG' nie wyglada na tag CalVer (oczekiwane np. 202606.1386)" >&2 + exit 1 + fi + if ! verify_tag_exists "$APPSERVER_REPO" "$TAG"; then + echo "BLAD: tag '$TAG' nie istnieje w $APPSERVER_REPO na Docker Hubie" >&2 + exit 1 + fi + VERSION="$TAG" + echo "Zaspawuje jawnie podana wersje: $VERSION" +else + echo "Odczytuje wersje z dzialajacego kontenera appserver..." + DIGEST="$(running_repo_digest appserver)" || { + echo "Podpowiedz: uruchom stack (make up) albo podaj wersje jawnie:" >&2 + echo " make zaspawaj-wersje TAG=202606.1386" >&2 + exit 1 + } + VERSION="$(resolve_digest_to_calver "$APPSERVER_REPO" "$DIGEST")" || { + echo "Podpowiedz: obraz nie pochodzi z Docker Huba albo jest starszy niz" >&2 + echo "100 ostatnich tagow. Podaj wersje jawnie: make zaspawaj-wersje TAG=..." >&2 + exit 1 + } + echo "appserver chodzi na ${APPSERVER_REPO}@${DIGEST} = ${VERSION}" + + # Sanity-check (best-effort): czy pozostale uslugi iplweb chodza na tej + # samej wersji? Kazde repo ma wlasne digesty, wiec porownujemy po + # rozwiazanych tagach CalVer. Rozjazd = tylko ostrzezenie. + for pair in authserver:bpp_authserver workerserver:bpp_workerserver \ + denorm-queue:bpp_denorm_queue celerybeat:bpp_beatserver; do + svc="${pair%%:*}"; repo="iplweb/${pair##*:}" + svc_digest="$(running_repo_digest "$svc" 2>/dev/null)" || { + echo " UWAGA: nie moge odczytac obrazu uslugi '$svc' - pomijam" + continue + } + svc_ver="$(resolve_digest_to_calver "$repo" "$svc_digest" 2>/dev/null)" || { + echo " UWAGA: nie moge rozwiazac wersji uslugi '$svc' - pomijam" + continue + } + if [ "$svc_ver" != "$VERSION" ]; then + echo " UWAGA: $svc chodzi na $svc_ver != $VERSION (appserver)." + echo " Spawam wedlug appservera; rozjazd wyrowna nastepne 'make up'." + fi + done +fi + +set_env_var "DOCKER_VERSION" "$VERSION" \ + "Wersja obrazow iplweb/bpp_* (zaspawana przez make zaspawaj-wersje)" + +echo "" +echo "Zaspawano DOCKER_VERSION=${VERSION} w ${ENV_FILE}." +echo "Nic nie zostalo zrestartowane - pin obowiazuje od nastepnej operacji" +echo "docker compose. Aktualizacja na nowsza wersje:" +echo " make zaspawaj-wersje TAG= && make pull && make up" From 94c86a9ecfde62ab48b3cb3dcc06b2a50f3a64bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Fri, 12 Jun 2026 15:02:49 +0200 Subject: [PATCH 05/11] plan: poprawka oczekiwanych liczb PASS (13 asercji w sekcji zaspawaj, nie 11) Co-Authored-By: Claude Fable 5 --- .../plans/2026-06-12-test-upgrade-zaspawaj-wersje.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/superpowers/plans/2026-06-12-test-upgrade-zaspawaj-wersje.md b/docs/superpowers/plans/2026-06-12-test-upgrade-zaspawaj-wersje.md index 4f6c55a..a2c7034 100644 --- a/docs/superpowers/plans/2026-06-12-test-upgrade-zaspawaj-wersje.md +++ b/docs/superpowers/plans/2026-06-12-test-upgrade-zaspawaj-wersje.md @@ -512,7 +512,7 @@ echo " make zaspawaj-wersje TAG= && make pull && make up" - [ ] **Step 4: Uruchom testy — mają przejść** Run: `bash scripts/test-docker-versions.sh` -Expected: `PASS=20 FAIL=0`, exit 0. +Expected: `PASS=22 FAIL=0`, exit 0. - [ ] **Step 5: Commit** @@ -610,7 +610,7 @@ assert_exit 0 "$rc" "test-upgrade.sh: poprawna skladnia (bash -n)" - [ ] **Step 2: Uruchom testy — sekcja `--clean` ma polec** Run: `bash scripts/test-docker-versions.sh` -Expected: dotychczasowe 20 PASS, FAIL-e w sekcji test-upgrade, exit != 0. +Expected: dotychczasowe 22 PASS, FAIL-e w sekcji test-upgrade, exit != 0. - [ ] **Step 3: Zaimplementuj `scripts/test-upgrade.sh`** @@ -849,7 +849,7 @@ fi - [ ] **Step 4: Uruchom testy — mają przejść** Run: `bash scripts/test-docker-versions.sh` -Expected: `PASS=25 FAIL=0`, exit 0. +Expected: `PASS=27 FAIL=0`, exit 0. - [ ] **Step 5: Commit** @@ -1056,4 +1056,4 @@ git commit -m "fix(test-upgrade): poprawki po weryfikacji" - **Spec coverage:** lib (Task 1), zaspawaj-wersje + tryby błędu (Task 2–3), test-upgrade + clean + gwarancje nienaruszalności + kontrola dysku (Task 4–5), docs + CLAUDE.md + mkdocs strict (Task 6), testy w konwencji repo + manualny staging (Task 1–4, 7). Kompatybilność wsteczna: żadnych nowych wymaganych zmiennych; `${DOCKER_VERSION:-latest}` nietknięte. - **Placeholder scan:** brak TBD/TODO; każdy krok ma pełny kod lub dokładną komendę. -- **Type consistency:** nazwy funkcji lib (`resolve_latest_calver`, `resolve_digest_to_calver`, `verify_tag_exists`, `running_repo_digest`) i zmiennych shadow (`bpp-shadow-*`) spójne między taskami; liczby PASS narastają 9 → 20 → 25. +- **Type consistency:** nazwy funkcji lib (`resolve_latest_calver`, `resolve_digest_to_calver`, `verify_tag_exists`, `running_repo_digest`) i zmiennych shadow (`bpp-shadow-*`) spójne między taskami; liczby PASS narastają 9 → 22 → 27. From 0d29ea4354ff7a0f82e56970d4719603405ab3a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Fri, 12 Jun 2026 15:04:28 +0200 Subject: [PATCH 06/11] feat(make): targety zaspawaj-wersje + test-docker-versions Co-Authored-By: Claude Fable 5 --- Makefile | 2 ++ mk/configs.mk | 8 +++++++- mk/misc.mk | 5 ++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index d938442..141872f 100644 --- a/Makefile +++ b/Makefile @@ -145,6 +145,8 @@ help: @echo " test-letsencrypt - Unit-tests dla scripts/letsencrypt.sh (mocked docker, no network)" @echo " validate-env-quotes - Sprawdz czy .env nie zawiera wartosci w cudzyslowach" @echo " fix-env-quotes - Auto-strip cudzyslowy z .env (z backupem .bak.)" + @echo " zaspawaj-wersje - Przypnij DOCKER_VERSION do wersji dzialajacego appservera (lub TAG=...)" + @echo " test-docker-versions - Unit-testy logiki wersji obrazow (mock curl/docker, no network)" @echo "" @echo " Host management:" @echo " base-host-update-upgrade - Update system packages" diff --git a/mk/configs.mk b/mk/configs.mk index 9f17d1a..3b4f426 100644 --- a/mk/configs.mk +++ b/mk/configs.mk @@ -1,4 +1,4 @@ -.PHONY: update-ssl-certs generate-grafana-datasources update-configs configure-resources ensure-config-files +.PHONY: update-ssl-certs generate-grafana-datasources update-configs configure-resources ensure-config-files zaspawaj-wersje # Non-interactive guard przed `make up`: dokopiowuje brakujace pliki z defaults/ # do $BPP_CONFIGS_DIR (np. gdy nowy release dodaje kolejny bind-mount, a user @@ -24,3 +24,9 @@ update-configs: generate-grafana-datasources configure-resources: @./scripts/configure-resources.sh + +# Przypina DOCKER_VERSION w $(BPP_CONFIGS_DIR)/.env do wersji CalVer, na +# ktorej faktycznie chodzi appserver (lub jawnej: TAG=202606.1386). +# Szczegoly i kontrakt: docs/eksploatacja/komendy.md, CLAUDE.md. +zaspawaj-wersje: + @TAG="$(TAG)" bash scripts/zaspawaj-wersje.sh diff --git a/mk/misc.mk b/mk/misc.mk index e8d95c7..1cc0d1d 100644 --- a/mk/misc.mk +++ b/mk/misc.mk @@ -1,4 +1,4 @@ -.PHONY: clean wait debug-show-current-settings +.PHONY: clean wait debug-show-current-settings test-docker-versions clean: -find . -name '*~' -o -name '\#*' -o -name '.*~' | xargs rm -f @@ -8,3 +8,6 @@ wait: debug-show-current-settings: docker compose exec appserver python src/manage.py debug_setup_initial_data --show-current + +test-docker-versions: + @bash scripts/test-docker-versions.sh From ef2c384f1108dcb581fd6764f990a298fbde1b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Fri, 12 Jun 2026 15:07:48 +0200 Subject: [PATCH 07/11] feat(test-upgrade): proba generalna migracji kandydata na shadow stacku Co-Authored-By: Claude Fable 5 --- scripts/test-docker-versions.sh | 15 +++ scripts/test-upgrade.sh | 227 ++++++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100755 scripts/test-upgrade.sh diff --git a/scripts/test-docker-versions.sh b/scripts/test-docker-versions.sh index 3029621..618223d 100755 --- a/scripts/test-docker-versions.sh +++ b/scripts/test-docker-versions.sh @@ -204,6 +204,21 @@ rc=0; BPP_CONFIGS_DIR="$cfg" MOCK_APPSERVER_RUNNING=0 bash "$ZW" >/dev/null 2>&1 assert_nonzero "$rc" "zaspawaj: appserver nie dziala -> exit != 0" assert_file_not_contains "$cfg/.env" "DOCKER_VERSION" "zaspawaj: .env nietkniety gdy appserver nie dziala" +# ===================== Testy test-upgrade.sh (--clean) ===================== +echo "" +echo "== test-upgrade.sh --clean ==" +TU="$REPO_DIR/scripts/test-upgrade.sh" + +: > "$DOCKER_LOG" +rc=0; bash "$TU" --clean >/dev/null 2>&1 || rc=$? +assert_exit 0 "$rc" "test-upgrade --clean: exit 0" +assert_file_contains "$DOCKER_LOG" "rm -f bpp-shadow-dbserver bpp-shadow-redis" "--clean: usuwa kontenery shadow" +assert_file_contains "$DOCKER_LOG" "volume rm -f bpp-shadow-pgdata" "--clean: usuwa wolumen shadow" +assert_file_contains "$DOCKER_LOG" "network rm bpp-shadow" "--clean: usuwa siec shadow" + +rc=0; bash -n "$TU" || rc=$? +assert_exit 0 "$rc" "test-upgrade.sh: poprawna skladnia (bash -n)" + # ======================= Podsumowanie ======================= echo "" echo "Wynik: PASS=$PASS FAIL=$FAIL" diff --git a/scripts/test-upgrade.sh b/scripts/test-upgrade.sh new file mode 100755 index 0000000..bf5a963 --- /dev/null +++ b/scripts/test-upgrade.sh @@ -0,0 +1,227 @@ +#!/usr/bin/env bash +set -euo pipefail +# +# Proba generalna aktualizacji: czy migracje obrazu-kandydata przechodza na +# kopii produkcyjnej bazy? Dziala CALKOWICIE obok produkcji: +# - shadow stack (dbserver+redis) czystym `docker run`, poza projektem +# Compose, na wlasnej sieci bpp-shadow, +# - pull kandydata PO TAGU WERSJI - lokalny tag :latest produkcji +# pozostaje nietkniety, +# - zero zapisu do .env, zero operacji na kontenerach/wolumenach produkcji. +# +# Uzycie: +# make test-upgrade # kandydat = najnowszy CalVer z Huba +# make test-upgrade TAG=202606.1386 # jawny kandydat +# make test-upgrade-clean # sprzatniecie shadow stacka +# +# Wynik: exit 0 = migracje przechodza (shadow posprzatany); +# exit != 0 = blad (shadow ZOSTAJE do inspekcji). + +REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" +# shellcheck source=scripts/lib-docker-versions.sh +. "$REPO_DIR/scripts/lib-docker-versions.sh" + +SHADOW_NET="bpp-shadow" +SHADOW_DB="bpp-shadow-dbserver" +SHADOW_REDIS="bpp-shadow-redis" +SHADOW_VOL="bpp-shadow-pgdata" +PARALLEL_JOBS="${PARALLEL_JOBS:-4}" +# Limity zasobow shadow stacka - przyciete, zeby nie zaglodzic produkcji. +SHADOW_DB_MEM="${SHADOW_DB_MEM:-1g}" +SHADOW_DB_CPUS="${SHADOW_DB_CPUS:-1.0}" +SHADOW_REDIS_MEM="${SHADOW_REDIS_MEM:-256m}" +SHADOW_MIGRATE_MEM="${SHADOW_MIGRATE_MEM:-2g}" + +cleanup_shadow() { + docker rm -f "$SHADOW_DB" "$SHADOW_REDIS" >/dev/null 2>&1 || true + docker volume rm -f "$SHADOW_VOL" >/dev/null 2>&1 || true + docker network rm "$SHADOW_NET" >/dev/null 2>&1 || true +} + +if [ "${1:-}" = "--clean" ]; then + echo "Sprzatam shadow stack ($SHADOW_DB, $SHADOW_REDIS, $SHADOW_VOL, $SHADOW_NET)..." + cleanup_shadow + echo "OK." + exit 0 +fi + +print_inspect_help() { + echo "" >&2 + echo "Shadow stack ZOSTAJE do inspekcji:" >&2 + echo " docker exec -it $SHADOW_DB psql -U \"\$DJANGO_BPP_DB_USER\" -d \"\$DJANGO_BPP_DB_NAME\"" >&2 + echo "Sprzatniecie: make test-upgrade-clean" >&2 +} +trap print_inspect_help ERR + +# --- BPP_CONFIGS_DIR / ENV_FILE --- +if [ -z "${BPP_CONFIGS_DIR:-}" ] && [ -f "$REPO_DIR/.env" ]; then + BPP_CONFIGS_DIR="$(grep -E '^BPP_CONFIGS_DIR=' "$REPO_DIR/.env" | tail -1 | cut -d= -f2-)" +fi +if [ -z "${BPP_CONFIGS_DIR:-}" ]; then + echo "BLAD: BPP_CONFIGS_DIR nie jest ustawione (brak $REPO_DIR/.env?)" >&2 + exit 1 +fi +export BPP_CONFIGS_DIR +ENV_FILE="$BPP_CONFIGS_DIR/.env" +if [ ! -f "$ENV_FILE" ]; then + echo "BLAD: brak pliku $ENV_FILE" >&2 + exit 1 +fi + +# --- Helper .env (kopia per-skrypt; konwencja: init-configs.sh) --- +get_env_var() { + local raw + raw="$(grep -E "^${1}=" "$ENV_FILE" 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" +} + +DB_NAME="$(get_env_var DJANGO_BPP_DB_NAME)" +DB_USER="$(get_env_var DJANGO_BPP_DB_USER)" +DB_PASSWORD="$(get_env_var DJANGO_BPP_DB_PASSWORD)" +if [ -z "$DB_NAME" ] || [ -z "$DB_USER" ] || [ -z "$DB_PASSWORD" ]; then + echo "BLAD: brak DJANGO_BPP_DB_NAME/USER/PASSWORD w $ENV_FILE" >&2 + exit 1 +fi + +# Wersja PG: ta sama logika dwuwarstwowego fallbacku co docker-compose.database.yml. +PG_VERSION="$(get_env_var DJANGO_BPP_POSTGRESQL_VERSION)" +[ -n "$PG_VERSION" ] || PG_VERSION="$(get_env_var DJANGO_BPP_DBSERVER_PG_VERSION)" +[ -n "$PG_VERSION" ] || PG_VERSION="16.13" + +# Katalog backupow: ta sama logika fallbacku co mk/database.mk. +BACKUP_DIR="$(get_env_var DJANGO_BPP_HOST_BACKUP_DIR)" +[ -n "$BACKUP_DIR" ] || BACKUP_DIR="$(get_env_var DJANGO_BPP_BACKUP_DIR)" +[ -n "$BACKUP_DIR" ] || BACKUP_DIR="$(cd "$BPP_CONFIGS_DIR/.." && pwd)/backups" +mkdir -p "$BACKUP_DIR" + +# Wersja redisa: ta sama co produkcyjna (z compose - zero driftu). +REDIS_IMAGE="$(grep -Eo 'redis:[0-9][0-9.]*' "$REPO_DIR/docker-compose.infrastructure.yml" | head -1)" +[ -n "$REDIS_IMAGE" ] || REDIS_IMAGE="redis:8.6.2" + +APPSERVER_REPO="iplweb/bpp_appserver" +cd "$REPO_DIR" + +# --- [1/6] Kandydat --- +echo "=== [1/6] Rozwiazuje obraz-kandydata ===" +if [ -n "${TAG:-}" ]; then + if ! printf '%s' "$TAG" | grep -qE "$CALVER_RE"; then + echo "BLAD: TAG='$TAG' nie wyglada na tag CalVer (np. 202606.1386)" >&2 + exit 1 + fi + CANDIDATE="$TAG" +else + CANDIDATE="$(resolve_latest_calver "$APPSERVER_REPO")" +fi +echo "Kandydat: ${APPSERVER_REPO}:${CANDIDATE}" +# Pull po tagu wersji - lokalny :latest produkcji nietkniety. +docker pull "${APPSERVER_REPO}:${CANDIDATE}" + +# --- [2/6] Kontrola miejsca na dysku --- +echo "=== [2/6] Kontrola miejsca na dysku ===" +if [ "${SKIP_DISK_CHECK:-0}" != "1" ]; then + DB_SIZE_MB="$(docker compose exec -T dbserver psql -U "$DB_USER" -d "$DB_NAME" \ + -tAc "SELECT pg_database_size('$DB_NAME')/1024/1024;" | tr -d '[:space:]')" + NEED_MB=$(( DB_SIZE_MB * 5 / 2 )) # ~2.5x: dump + untar + shadow volume + FREE_BACKUP_MB="$(df -Pm "$BACKUP_DIR" | awk 'NR==2 {print $4}')" + DOCKER_ROOT="$(docker info --format '{{.DockerRootDir}}' 2>/dev/null || echo /var/lib/docker)" + FREE_DOCKER_MB="$(df -Pm "$DOCKER_ROOT" 2>/dev/null | awk 'NR==2 {print $4}')" + [ -n "$FREE_DOCKER_MB" ] || FREE_DOCKER_MB="$FREE_BACKUP_MB" + echo "Baza: ${DB_SIZE_MB} MB; wymagane ~${NEED_MB} MB wolnego miejsca." + if [ "$FREE_BACKUP_MB" -lt "$NEED_MB" ] || [ "$FREE_DOCKER_MB" -lt "$NEED_MB" ]; then + echo "BLAD: za malo miejsca (backup dir: ${FREE_BACKUP_MB} MB, docker root: ${FREE_DOCKER_MB} MB)." >&2 + echo "Wymuszenie pominiecia kontroli: SKIP_DISK_CHECK=1 make test-upgrade" >&2 + exit 1 + fi +else + echo "(pominieta: SKIP_DISK_CHECK=1)" +fi + +# --- [3/6] Backup produkcyjnej bazy --- +echo "=== [3/6] Backup produkcyjnej bazy (make db-backup) ===" +make -C "$REPO_DIR" db-backup +BACKUP_TAR_PATH="$(ls -t "$BACKUP_DIR"/db-backup-*.tar.gz 2>/dev/null | head -1)" +if [ -z "$BACKUP_TAR_PATH" ]; then + echo "BLAD: nie znalazlem swiezego dumpa w $BACKUP_DIR" >&2 + exit 1 +fi +BACKUP_TAR="$(basename "$BACKUP_TAR_PATH")" +BACKUP_DIRNAME="${BACKUP_TAR%.tar.gz}" +echo "Dump: $BACKUP_TAR_PATH" + +# --- [4/6] Shadow stack --- +echo "=== [4/6] Stawiam shadow stack (siec $SHADOW_NET) ===" +cleanup_shadow # zombie z poprzedniego przebiegu +docker network create "$SHADOW_NET" >/dev/null +docker volume create "$SHADOW_VOL" >/dev/null +docker run -d --name "$SHADOW_DB" --network "$SHADOW_NET" \ + -e POSTGRES_DB="$DB_NAME" \ + -e POSTGRES_USER="$DB_USER" \ + -e POSTGRES_PASSWORD="$DB_PASSWORD" \ + -v "$SHADOW_VOL":/var/lib/postgresql/data \ + -v "$BACKUP_DIR":/backup:ro \ + --memory "$SHADOW_DB_MEM" --cpus "$SHADOW_DB_CPUS" \ + "iplweb/bpp_dbserver:psql-${PG_VERSION}" >/dev/null +docker run -d --name "$SHADOW_REDIS" --network "$SHADOW_NET" \ + --memory "$SHADOW_REDIS_MEM" \ + "$REDIS_IMAGE" >/dev/null + +echo "Czekam na gotowosc shadow-postgresa..." +for i in $(seq 1 60); do + if docker exec "$SHADOW_DB" pg_isready -U "$DB_USER" -d "$DB_NAME" >/dev/null 2>&1; then + break + fi + if [ "$i" -eq 60 ]; then + echo "BLAD: shadow-postgres nie wstal w 120 s" >&2 + exit 1 + fi + sleep 2 +done + +# --- [5/6] Restore dumpa do shadow-bazy --- +echo "=== [5/6] Restore dumpa do shadow-bazy (pg_restore -j $PARALLEL_JOBS) ===" +docker exec "$SHADOW_DB" mkdir -p /tmp/restore +docker exec "$SHADOW_DB" tar xzf "/backup/$BACKUP_TAR" -C /tmp/restore +docker exec "$SHADOW_DB" pg_restore -Fd -j "$PARALLEL_JOBS" --no-owner \ + -U "$DB_USER" -d "$DB_NAME" "/tmp/restore/$BACKUP_DIRNAME" + +# --- [6/6] Migracja obrazem-kandydatem --- +echo "=== [6/6] manage.py migrate obrazem ${APPSERVER_REPO}:${CANDIDATE} ===" +# Entrypoint nadpisany: zadnych faz startowych (staticfiles, gunicorn) - +# wylacznie migracja. --env-file daje komplet zmiennych jak w produkcji, +# -e nadpisuje hosty na shadow. +set +e +docker run --rm --network "$SHADOW_NET" \ + --env-file "$ENV_FILE" \ + -e DJANGO_BPP_DB_HOST="$SHADOW_DB" \ + -e DJANGO_BPP_DB_PORT=5432 \ + -e DJANGO_BPP_REDIS_HOST="$SHADOW_REDIS" \ + --memory "$SHADOW_MIGRATE_MEM" \ + --entrypoint python \ + "${APPSERVER_REPO}:${CANDIDATE}" src/manage.py migrate --noinput +MIGRATE_RC=$? +set -e + +if [ "$MIGRATE_RC" -eq 0 ]; then + trap - ERR + echo "" + echo "=== OK: migracje ${CANDIDATE} przechodza na kopii produkcyjnej bazy ===" + echo "Sprzatam shadow stack..." + cleanup_shadow + echo "Gotowe. Produkcja przez caly czas byla nietknieta." + exit 0 +else + echo "" >&2 + echo "=== BLAD: migracja ${CANDIDATE} NIE przeszla (exit=$MIGRATE_RC) ===" >&2 + echo "Shadow stack ZOSTAJE do inspekcji:" >&2 + echo " docker exec -it $SHADOW_DB psql -U $DB_USER -d $DB_NAME" >&2 + echo "Ponowna proba migracji (po obejrzeniu):" >&2 + echo " TAG=$CANDIDATE make test-upgrade" >&2 + echo "Sprzatniecie: make test-upgrade-clean" >&2 + exit 1 +fi From c002e33381e9263cf92c0df2d06ad39f8eb9363f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Fri, 12 Jun 2026 15:10:03 +0200 Subject: [PATCH 08/11] feat(make): targety test-upgrade + test-upgrade-clean Co-Authored-By: Claude Fable 5 --- Makefile | 2 ++ mk/deployment.mk | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 141872f..9d1027c 100644 --- a/Makefile +++ b/Makefile @@ -92,6 +92,8 @@ help: @echo " refresh - Full refresh (prune, pull, recreate)" @echo " restart-appserver - Restart application server only" @echo " wait - Wait for Docker build, then pull and restart" + @echo " test-upgrade - Proba generalna: migracje kandydata na kopii bazy (TAG=...)" + @echo " test-upgrade-clean - Sprzatniecie shadow stacka po nieudanym test-upgrade" @echo "" @echo " Database:" @echo " migrate - Run Django migrations (stops workers safely)" diff --git a/mk/deployment.mk b/mk/deployment.mk index e6b5ac3..3943e11 100644 --- a/mk/deployment.mk +++ b/mk/deployment.mk @@ -1,4 +1,4 @@ -.PHONY: all run refresh up up-quick up-appserver up-webserver stop rmrf restart restart-appserver health check-quic validate-env-quotes fix-env-quotes test-validate-env-quotes +.PHONY: all run refresh up up-quick up-appserver up-webserver stop rmrf restart restart-appserver health check-quic validate-env-quotes fix-env-quotes test-validate-env-quotes test-upgrade test-upgrade-clean all: run @@ -78,3 +78,13 @@ check-quic: @bash scripts/check-quic-port.sh $(HOST) run: pull build update-configs up test-email + +# Proba generalna aktualizacji: backup -> shadow stack (dbserver+redis poza +# projektem Compose) -> restore -> migrate obrazem-kandydatem. Produkcja +# (kontenery, wolumeny, lokalny tag :latest, .env) pozostaje nietknieta. +# Uwaga: NIE mylic z test-upgrade-postgres (unit-testy upgrade'u PG). +test-upgrade: validate-env-quotes + @TAG="$(TAG)" bash scripts/test-upgrade.sh + +test-upgrade-clean: + @bash scripts/test-upgrade.sh --clean From b5d883eef856dcbdbbfb1e6c08f536691d981272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Fri, 12 Jun 2026 15:16:09 +0200 Subject: [PATCH 09/11] docs: zaspawaj-wersje + test-upgrade (komendy.md, CLAUDE.md) Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 16 +++++++++ docs/eksploatacja/komendy.md | 63 ++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 0d972b4..4d8d050 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -85,6 +85,22 @@ User uploads land in the `media` volume mounted at `/mediaroot` in every Django `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`. +### Image version pinning (`DOCKER_VERSION`) and upgrade rehearsal + +`DOCKER_VERSION` pins the 5 `iplweb/bpp_*` images (default `latest` — compose +fallback `${DOCKER_VERSION:-latest}` must stay for backwards compat). +`make zaspawaj-wersje` welds the version **actually running in the appserver +container** (not the local `latest` tag) into `.env` via the stable +`set_env_var` helper; updating a pinned host requires an explicit +`make zaspawaj-wersje TAG=`. `make test-upgrade` is the migration +rehearsal: fresh `db-backup` → shadow stack (`bpp-shadow-*`, plain +`docker run` outside the Compose project) → `pg_restore` → candidate-image +`manage.py migrate` with overridden entrypoint. It must never touch +production containers, volumes, the local `latest` tag, or `.env`. Candidate +images are pulled **by version tag**, never via `:latest`. Shared +digest↔CalVer logic lives in `scripts/lib-docker-versions.sh` +(tests: `make test-docker-versions`). Detail: `docs/eksploatacja/komendy.md`. + ## Critical Deployment Patterns ### Running commands in containers diff --git a/docs/eksploatacja/komendy.md b/docs/eksploatacja/komendy.md index 2c870f5..a16c83f 100644 --- a/docs/eksploatacja/komendy.md +++ b/docs/eksploatacja/komendy.md @@ -120,3 +120,66 @@ make base-host-update-upgrade # Aktualizacja systemu (apt update + full-upgrade make base-host-reboot # Restart hosta make install-docker # Instalacja Dockera na hoście ``` + +## Aktualizacje i wersje obrazów + +### `make zaspawaj-wersje` — pinowanie wersji obrazów iplweb + +Domyślnie obrazy `iplweb/bpp_*` jadą na ruchomym tagu `latest` — każdy +`make pull` może podmienić wersję. `zaspawaj-wersje` utrwala w +`$BPP_CONFIGS_DIR/.env` zmienną `DOCKER_VERSION=` odpowiadającą +wersji, na której **faktycznie chodzi** kontener `appserver` (nie tej, na +którą wskazuje lokalny tag `latest` — po `make pull` bez recreate te dwie +mogą się różnić). + +```bash +make zaspawaj-wersje # wersja z działającego appservera +make zaspawaj-wersje TAG=202606.1386 # jawny tag +``` + +Po zaspawaniu `restart`, awaryjny recreate i nocne restarty Ofelii trzymają +się przypiętej wersji. Aktualizacja na nowszą wersję wymaga jawnej decyzji: + +```bash +make zaspawaj-wersje TAG= && make pull && make up +``` + +Zmienna obejmuje 5 obrazów iplweb (`bpp_appserver`, `bpp_authserver`, +`bpp_workerserver`, `bpp_denorm_queue`, `bpp_beatserver`). Pozostałe obrazy +(nginx, redis, grafana, …) są przypięte na sztywno w plikach compose; +PostgreSQL ma własną `DJANGO_BPP_POSTGRESQL_VERSION`. + +### `make test-upgrade` — próba generalna migracji + +Sprawdza, czy migracje bazodanowe obrazu-kandydata przechodzą na **kopii +produkcyjnej bazy**, zanim czegokolwiek dotkniesz na produkcji: + +1. pobiera obraz-kandydat **po tagu wersji** (lokalny `latest` nietknięty), +2. robi świeży `make db-backup` (błąd backupu przerywa całość), +3. stawia shadow stack (`bpp-shadow-dbserver` + `bpp-shadow-redis`) na + osobnej sieci, poza projektem Compose, z przyciętymi limitami zasobów, +4. restoruje dump do shadow-bazy, +5. uruchamia `manage.py migrate` obrazem-kandydatem (entrypoint nadpisany — + nic poza migracją się nie uruchamia). + +```bash +make test-upgrade # kandydat = najnowszy tag CalVer z Docker Huba +make test-upgrade TAG=202606.1386 # jawny kandydat +``` + +**Sukces** → shadow stack jest sprzątany, exit 0. **Porażka** → shadow stack +zostaje do inspekcji (`docker exec -it bpp-shadow-dbserver psql ...`); +sprzątasz przez `make test-upgrade-clean`. + +Wymagania: wolne miejsce na dysku ≈ 2,5× rozmiar bazy (kontrolowane przed +startem; wymuszenie pominięcia kontroli: `SKIP_DISK_CHECK=1`). Próba +obciąża CPU/IO hosta na czas dump+restore — na małych hostach uruchamiaj +poza godzinami szczytu. + +Typowy przepływ bezpiecznej aktualizacji na zaspawanym hoście: + +```bash +make test-upgrade # migracje kandydata przechodzą? +make zaspawaj-wersje TAG= # przypnij nową wersję +make pull && make up # właściwy deploy (health-gate --wait) +``` From 43ea73346bb0088c6281a84f6df8fab2e0fbd5b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Fri, 12 Jun 2026 15:37:57 +0200 Subject: [PATCH 10/11] =?UTF-8?q?docs:=20dedykowana=20strona=20Aktualizacj?= =?UTF-8?q?e=20i=20wersje=20obraz=C3=B3w=20(workflow),=20komendy.md=20jako?= =?UTF-8?q?=20referencja?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- docs/eksploatacja/aktualizacje.md | 140 ++++++++++++++++++++++++++++++ docs/eksploatacja/komendy.md | 64 ++------------ mkdocs.yml | 1 + 3 files changed, 148 insertions(+), 57 deletions(-) create mode 100644 docs/eksploatacja/aktualizacje.md diff --git a/docs/eksploatacja/aktualizacje.md b/docs/eksploatacja/aktualizacje.md new file mode 100644 index 0000000..7f680e0 --- /dev/null +++ b/docs/eksploatacja/aktualizacje.md @@ -0,0 +1,140 @@ +# Aktualizacje i wersje obrazów + +Jak bezpiecznie aktualizować obrazy `iplweb/bpp_*` na działającej instalacji: +przypięcie wersji (`make zaspawaj-wersje`), próba generalna migracji na kopii +produkcyjnej bazy (`make test-upgrade`) i zalecany przepływ aktualizacji. + +## Problem: ruchomy tag `latest` + +Domyślnie obrazy `iplweb/bpp_*` jadą na tagu `latest` +(`${DOCKER_VERSION:-latest}` w plikach compose). To wygodne, ale ma dwie +konsekwencje: + +- **każdy `make pull` może podmienić wersję** — także "przy okazji", gdy + chodziło tylko o restart; +- **nie wiadomo, co dokładnie jest wdrożone** — dwa hosty robiące deploy + w odstępie godziny mogą dostać różne obrazy, a po awarii trudno wskazać + wersję, do której należałoby wrócić. + +Obrazy spoza rodziny iplweb (nginx, redis, grafana, netdata, …) są przypięte +na sztywno w plikach compose i nie podlegają temu mechanizmowi; PostgreSQL ma +własną zmienną `DJANGO_BPP_POSTGRESQL_VERSION` +([PostgreSQL — wersje i upgrade](../konfiguracja/postgresql.md)). + +## `make zaspawaj-wersje` — przypięcie wersji + +```bash +make zaspawaj-wersje # wersja z działającego appservera +make zaspawaj-wersje TAG=202606.1386 # jawny tag +``` + +Target utrwala w `$BPP_CONFIGS_DIR/.env` zmienną +`DOCKER_VERSION=` odpowiadającą wersji, na której **faktycznie +chodzi** kontener `appserver`. Celowo nie patrzy na lokalny tag `latest`: +po `make pull` bez recreate lokalny `latest` może już wskazywać nowszy, +nieprzetestowany obraz — zaspawanie ma przybić stan faktyczny produkcji, +nie stan cache'u obrazów. + +Wersja jest rozwiązywana z digestu działającego kontenera przez API Docker +Huba (tagi CalVer postaci `RRRRMM.NNNN`, np. `202606.1386`). Przy okazji +target sprawdza, czy pozostałe kontenery iplweb (`authserver`, +`workerserver`, `denorm-queue`, `celerybeat`) chodzą na tej samej wersji — +rozjazd to tylko ostrzeżenie (wyrówna go następne `make up`). + +Po zaspawaniu: + +- `make restart`, awaryjny recreate i nocne restarty Ofelii trzymają się + przypiętej wersji — nic nie wjedzie "samo"; +- nowa wersja wymaga **jawnej decyzji**: + +```bash +make zaspawaj-wersje TAG= && make pull && make up +``` + +Nic nie jest restartowane w momencie zaspawania — pin obowiązuje od +następnej operacji compose. Host bez zaspawania (brak `DOCKER_VERSION` +w `.env`) działa po staremu, na `latest`. + +## `make test-upgrade` — próba generalna migracji + +Najczęstszy scenariusz katastrofy przy aktualizacji to nowy obraz, którego +migracje bazodanowe nie przechodzą — wykrywany dopiero w trakcie deployu, +gdy stare kontenery już nie działają. `test-upgrade` wykrywa go **obok** +produkcji, na świeżej kopii produkcyjnych danych: + +```bash +make test-upgrade # kandydat = najnowszy tag CalVer z Docker Huba +make test-upgrade TAG=202606.1386 # jawny kandydat +``` + +Przebieg: + +1. **Kandydat** — obraz pobierany **po tagu wersji**, nigdy przez `:latest` + (lokalny `latest`, na którym chodzi produkcja, pozostaje nietknięty). +2. **Kontrola miejsca** — wymagane ≈ 2,5× rozmiaru bazy (dump + rozpakowanie + + shadow-wolumen); brak miejsca przerywa próbę zanim cokolwiek ruszy. + Wymuszenie pominięcia: `SKIP_DISK_CHECK=1 make test-upgrade`. +3. **Backup** — świeży `make db-backup`; błąd backupu przerywa całość. +4. **Shadow stack** — `bpp-shadow-dbserver` (ta sama wersja PostgreSQL co + produkcja) + `bpp-shadow-redis` na osobnej sieci dockerowej `bpp-shadow`, + poza projektem Compose, z przyciętymi limitami zasobów. +5. **Restore** dumpa do shadow-bazy (`pg_restore -j`). +6. **Migracja** — `manage.py migrate` obrazem-kandydatem z nadpisanym + entrypointem: nic poza migracją się nie uruchamia. + +Wynik: + +- **Sukces (exit 0)** — komunikat, pełne sprzątnięcie shadow stacka. + Produkcja przez cały czas była nietknięta. +- **Porażka (exit 1)** — shadow stack **zostaje** do inspekcji: + +```bash +docker exec -it bpp-shadow-dbserver psql -U $DJANGO_BPP_DB_USER -d $DJANGO_BPP_DB_NAME +make test-upgrade-clean # sprzątnięcie po obejrzeniu +``` + +Gwarancje: próba nie dotyka kontenerów ani wolumenów produkcji, nie zmienia +lokalnego tagu `latest`, nie zapisuje niczego do `.env`. Jedyny koszt to +obciążenie CPU/IO podczas dump+restore — na małych hostach uruchamiaj poza +godzinami szczytu. + +Limity zasobów shadow stacka można nadpisać zmiennymi środowiskowymi: +`SHADOW_DB_MEM` (domyślnie `1g`), `SHADOW_DB_CPUS` (`1.0`), +`SHADOW_REDIS_MEM` (`256m`), `SHADOW_MIGRATE_MEM` (`2g`), +`PARALLEL_JOBS` (`4`, liczba wątków pg_restore). + +## Zalecany przepływ aktualizacji + +Na zaspawanym hoście: + +```bash +make test-upgrade # 1. migracje kandydata przechodzą? +make zaspawaj-wersje TAG= # 2. przypnij nową wersję +make pull && make up # 3. właściwy deploy (health-gate --wait) +``` + +Kolejność jest istotna: dopiero po udanej próbie generalnej przypinamy +kandydata i dotykamy produkcji. `make up` używa `--wait`, więc niewstający +appserver zwróci błąd zamiast cicho zostawić niedziałający stack. + +## Powrót po nieudanej aktualizacji + +Zaspawanie czyni ręczny rollback przewidywalnym: stara wersja jest zapisana +w historii `.env` (i w outputach `zaspawaj-wersje`), a świeży dump leży +w katalogu backupów. + +```bash +make zaspawaj-wersje TAG= # wróć do poprzedniej wersji obrazów +make pull && make up +make restore # tylko gdy migracja zdążyła zmienić schemę +``` + +`make restore` cofa też dane wpisane po backupie — używaj go wyłącznie, gdy +nowa migracja faktycznie zmieniła schemę w sposób niekompatybilny ze starym +obrazem. Szczegóły restore: [Backup i rclone](backup-i-rclone.md). + +## Zobacz też + +- [Najważniejsze komendy](komendy.md) — skrócona referencja targetów +- [Backup i rclone](backup-i-rclone.md) — skąd bierze się dump używany przez próbę +- [PostgreSQL — wersje i upgrade](../konfiguracja/postgresql.md) — upgrade samej bazy diff --git a/docs/eksploatacja/komendy.md b/docs/eksploatacja/komendy.md index a16c83f..4e98b07 100644 --- a/docs/eksploatacja/komendy.md +++ b/docs/eksploatacja/komendy.md @@ -123,63 +123,13 @@ make install-docker # Instalacja Dockera na hoście ## Aktualizacje i wersje obrazów -### `make zaspawaj-wersje` — pinowanie wersji obrazów iplweb - -Domyślnie obrazy `iplweb/bpp_*` jadą na ruchomym tagu `latest` — każdy -`make pull` może podmienić wersję. `zaspawaj-wersje` utrwala w -`$BPP_CONFIGS_DIR/.env` zmienną `DOCKER_VERSION=` odpowiadającą -wersji, na której **faktycznie chodzi** kontener `appserver` (nie tej, na -którą wskazuje lokalny tag `latest` — po `make pull` bez recreate te dwie -mogą się różnić). - -```bash -make zaspawaj-wersje # wersja z działającego appservera -make zaspawaj-wersje TAG=202606.1386 # jawny tag -``` - -Po zaspawaniu `restart`, awaryjny recreate i nocne restarty Ofelii trzymają -się przypiętej wersji. Aktualizacja na nowszą wersję wymaga jawnej decyzji: - -```bash -make zaspawaj-wersje TAG= && make pull && make up -``` - -Zmienna obejmuje 5 obrazów iplweb (`bpp_appserver`, `bpp_authserver`, -`bpp_workerserver`, `bpp_denorm_queue`, `bpp_beatserver`). Pozostałe obrazy -(nginx, redis, grafana, …) są przypięte na sztywno w plikach compose; -PostgreSQL ma własną `DJANGO_BPP_POSTGRESQL_VERSION`. - -### `make test-upgrade` — próba generalna migracji - -Sprawdza, czy migracje bazodanowe obrazu-kandydata przechodzą na **kopii -produkcyjnej bazy**, zanim czegokolwiek dotkniesz na produkcji: - -1. pobiera obraz-kandydat **po tagu wersji** (lokalny `latest` nietknięty), -2. robi świeży `make db-backup` (błąd backupu przerywa całość), -3. stawia shadow stack (`bpp-shadow-dbserver` + `bpp-shadow-redis`) na - osobnej sieci, poza projektem Compose, z przyciętymi limitami zasobów, -4. restoruje dump do shadow-bazy, -5. uruchamia `manage.py migrate` obrazem-kandydatem (entrypoint nadpisany — - nic poza migracją się nie uruchamia). - ```bash -make test-upgrade # kandydat = najnowszy tag CalVer z Docker Huba -make test-upgrade TAG=202606.1386 # jawny kandydat +make zaspawaj-wersje # Przypnij DOCKER_VERSION do wersji działającego appservera +make zaspawaj-wersje TAG=... # Przypnij jawnie podaną wersję (tag CalVer) +make test-upgrade # Próba generalna: migracje kandydata na kopii bazy +make test-upgrade TAG=... # Próba generalna jawnie wskazanego kandydata +make test-upgrade-clean # Sprzątnięcie shadow stacka po nieudanej próbie ``` -**Sukces** → shadow stack jest sprzątany, exit 0. **Porażka** → shadow stack -zostaje do inspekcji (`docker exec -it bpp-shadow-dbserver psql ...`); -sprzątasz przez `make test-upgrade-clean`. - -Wymagania: wolne miejsce na dysku ≈ 2,5× rozmiar bazy (kontrolowane przed -startem; wymuszenie pominięcia kontroli: `SKIP_DISK_CHECK=1`). Próba -obciąża CPU/IO hosta na czas dump+restore — na małych hostach uruchamiaj -poza godzinami szczytu. - -Typowy przepływ bezpiecznej aktualizacji na zaspawanym hoście: - -```bash -make test-upgrade # migracje kandydata przechodzą? -make zaspawaj-wersje TAG= # przypnij nową wersję -make pull && make up # właściwy deploy (health-gate --wait) -``` +Pełny opis przepływu bezpiecznej aktualizacji (pinowanie wersji, shadow stack, +rollback): [Aktualizacje i wersje obrazów](aktualizacje.md). diff --git a/mkdocs.yml b/mkdocs.yml index 677e258..360cbfc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -95,6 +95,7 @@ nav: - PostgreSQL — wersje i upgrade: konfiguracja/postgresql.md - Eksploatacja: - Najważniejsze komendy: eksploatacja/komendy.md + - Aktualizacje i wersje obrazów: eksploatacja/aktualizacje.md - Baza danych: eksploatacja/baza-danych.md - Backup i rclone: eksploatacja/backup-i-rclone.md - Przenosiny serwera: eksploatacja/przenosiny-serwera.md From b6aa1ce6ac9c3f883f7e9ee16226dd88ce21bde6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Fri, 12 Jun 2026 15:57:30 +0200 Subject: [PATCH 11/11] fix(shellcheck): dyrektywy disable dla SC2317/SC2012 (test-upgrade) i SC2088 false positive (init-configs) SC2317: print_inspect_help wolane posrednio przez trap ERR. SC2012: ls -t na kontrolowanych nazwach dumpow (db-backup-.tar.gz). SC2088: wzorce case dopasowuja literalna tylde z inputu usera (bez eval) - false positive wprowadzony na main w 10a44b2, naprawiony tutaj zeby CI przeszlo na merge'u PR. Co-Authored-By: Claude Fable 5 --- scripts/init-configs.sh | 1 + scripts/test-upgrade.sh | 2 ++ 2 files changed, 3 insertions(+) diff --git a/scripts/init-configs.sh b/scripts/init-configs.sh index 25ea242..5024b4b 100755 --- a/scripts/init-configs.sh +++ b/scripts/init-configs.sh @@ -35,6 +35,7 @@ INPUT_DIR="${INPUT_DIR:-$DEFAULT_CONFIG_DIR}" # Expand tilde (bez eval - wklejona sciezka z backtickami/`$()` nie moze # sie wykonac jako kod) +# shellcheck disable=SC2088 # wzorce case dopasowuja LITERALNA tylde z inputu usera case "$INPUT_DIR" in "~") INPUT_DIR="$HOME" ;; "~/"*) INPUT_DIR="$HOME/${INPUT_DIR#\~/}" ;; diff --git a/scripts/test-upgrade.sh b/scripts/test-upgrade.sh index bf5a963..cb27ff4 100755 --- a/scripts/test-upgrade.sh +++ b/scripts/test-upgrade.sh @@ -45,6 +45,7 @@ if [ "${1:-}" = "--clean" ]; then exit 0 fi +# shellcheck disable=SC2317 # wywolywane posrednio przez trap ERR print_inspect_help() { echo "" >&2 echo "Shadow stack ZOSTAJE do inspekcji:" >&2 @@ -145,6 +146,7 @@ fi # --- [3/6] Backup produkcyjnej bazy --- echo "=== [3/6] Backup produkcyjnej bazy (make db-backup) ===" make -C "$REPO_DIR" db-backup +# shellcheck disable=SC2012 # nazwy dumpow kontrolowane (db-backup-.tar.gz), bez bialych znakow BACKUP_TAR_PATH="$(ls -t "$BACKUP_DIR"/db-backup-*.tar.gz 2>/dev/null | head -1)" if [ -z "$BACKUP_TAR_PATH" ]; then echo "BLAD: nie znalazlem swiezego dumpa w $BACKUP_DIR" >&2