From 0f390c3888f248087d1ba6ef97a3143974187bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Wed, 21 Oct 2026 21:12:59 +0200 Subject: [PATCH 1/2] feat(config): rejestr zmiennych operatora + make configure Jeden rejestr (scripts/config-registry.tsv) zmiennych .env, o ktorych decyduje tylko operator: e-mail admina, SSL, Slack, Rollbar, Google, backupy, widgety Freshdesk (iplweb/bpp#868: WIDGET_ID, WIDGET_ID_ANON, WIDGET_JWT_SECRET). Korzystaja z niego: - init-configs (--init) - zamiast wlasnych pytan i ensure_env_var, - make up / up-quick / refresh (ask-missing-config) - pyta RAZ o brakujace; pusta odpowiedz zapisuje VAR= = decyzja "nie mamy", nie pyta ponownie; bez TTY / pod BPP_SKIP_HEALTH_GATE / BPP_NO_CONFIG_PROMPT nie pyta, timeout/EOF nic nie zapisuje, - make configure - menu grup, zapis na koncu, - make config-check + make doctor (pozycja 7) - walidacja wartosci, - make docs-config-table - tabela w docs/konfiguracja/zmienne.md (test pilnuje aktualnosci). Swiezy .env z init-configs ma ten sam zbior zmiennych co wczesniej (+ 3 Freshdesk); migracja DJANGO_BPP_BACKUP_DIR dziala jak dotad. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 + Makefile | 2 + docs/eksploatacja/komendy.md | 5 +- docs/konfiguracja/architektura.md | 35 +--- docs/konfiguracja/zmienne.md | 137 +++++++++++++ mk/configs.mk | 24 ++- mk/deployment.mk | 6 +- mkdocs.yml | 1 + scripts/config-registry.tsv | 35 ++++ scripts/configure.sh | 307 ++++++++++++++++++++++++++++++ scripts/doctor.sh | 7 +- scripts/init-configs.sh | 109 ++--------- scripts/lib-config-registry.sh | 188 ++++++++++++++++++ scripts/render-config-docs.sh | 36 ++++ scripts/test-doctor.sh | 8 +- tests/test_makefile.sh | 138 ++++++++++++++ 16 files changed, 912 insertions(+), 128 deletions(-) create mode 100644 docs/konfiguracja/zmienne.md create mode 100644 scripts/config-registry.tsv create mode 100644 scripts/configure.sh create mode 100644 scripts/lib-config-registry.sh create mode 100644 scripts/render-config-docs.sh diff --git a/CLAUDE.md b/CLAUDE.md index b059f87..810a207 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -458,6 +458,8 @@ All services (except `backup-runner` and `rclone` — **two** deliberate excepti **OIDC / Keycloak account binding** — `DJANGO_BPP_OIDC_GRACE_BIND` + `DJANGO_BPP_OIDC_TRUSTED_EMAIL_DOMAINS` (CSV) + `DJANGO_BPP_OIDC_GRACE_BIND_PRIVILEGED`, all off by default, all with a `DJANGO_BPP_OIDC__…` variant that wins over the bare one. **Symptom that identifies this situation:** Keycloak login completes end-to-end (302 → 302 → 200, **no 403 — the WAF is not involved**) but the user stays logged out, and `appserver` logs `OIDC: konto z tym adresem już istnieje — połącz je z SSO przez profil` plus an AXES failure on `/oidc/callback/`. Cause: BPP matches identities by `(issuer, sub)`, never by e-mail (matching by e-mail = account takeover), so pre-SSO accounts have no binding yet. The built-in remedy (profile → „Połącz konto z SSO") requires a **local password**, so it is a dead end on pure-SSO installs — that's what these flags are for. Trust comes from the **domain list**, not `email_verified`: LDAP realms put the institutional address in `mail` and a private one in `email`, and `email_verified` describes the latter. **Anti-fixes — do NOT:** clear the account's e-mail to dodge the collision (creates a second empty account, orphans the real one with its permissions/PBN token/linked `Autor`); make the backend match users by e-mail again; or set `GRACE_BIND_PRIVILEGED` without a domain list (it's a deliberate no-op — the list is the only gate protecting admin accounts). No Compose change needed: all three reach Django via the wholesale `env_file`. Needs a BPP image with `iplweb/bpp#753`; older images ignore them. Operator doc: `docs/konfiguracja/architektura.md`. +**Operator-variable registry** — `scripts/config-registry.tsv` (lib: `scripts/lib-config-registry.sh`, driver: `scripts/configure.sh`) is the single list of `.env` vars only the operator can decide: admin e-mail, SSL mode/LE e-mail, Slack, Rollbar, Google, backup dir/retention/rclone remote, Freshdesk widgets (`DJANGO_BPP_FRESHDESK_WIDGET_ID`/`_ID_ANON`/`_JWT_SECRET`, iplweb/bpp#868). It feeds `init-configs` (`--init`), `make up`/`up-quick`/`refresh` (`ask-missing-config` = `--missing`), `make configure` (group menu), `make config-check` + `make doctor` item 7 (`--check`) and the table in `docs/konfiguracja/zmienne.md` (`make docs-config-table`; staleness fails `test_configure_registry`). **A new ask-the-operator var = one TSV row + `make docs-config-table`** — no `ensure_env_var`, no migration; it still needs a default where it's read (Compose/Django), because autoupdate/CI never answer. Vars with their own logic (hostnames, DB, PG versions, `ADMINS`) stay in `init-configs.sh`. **The decision is the var's PRESENCE in `.env`, not its value:** an empty answer writes `VAR=` (= disabled) and is never asked again. **Anti-fixes — do NOT:** (1) change the check to "non-empty" — every `make up` would nag operators who deliberately said no; (2) write `--missing` answers one by one — EOF/timeout must leave `.env` untouched ("no answer" is not a decision); (3) let it prompt when `BPP_SKIP_HEALTH_GATE`/`BPP_NO_CONFIG_PROMPT` is set — screen gives autoupdate a pseudo-TTY, a prompt would hang the cycle while it holds the lock (a new non-interactive `make up` caller must set one); (4) read registry rows with `IFS=$'\t'` — tab is whitespace-IFS, two tabs collapse and an empty DOMYSLNA shifts every column (rows come out `\037`-separated); (5) read rows from stdin inside the prompt loop — stdin carries the answers (rows go through fd 3); (6) relax the `$`/`#`/quote ban — `.env` is also `-include`d by the Makefile. Operator doc: `docs/konfiguracja/zmienne.md`. + **html2docx fallback** — optional **HTTP sidecar** (`docker-compose.application.yml`, `profiles: ['html2docx']`), off by default. HTML→DOCX export normally uses pandoc from the appserver image; the sidecar is for hosts where pandoc core-dumps (VMWare ESX). Enabling is **two** opt-ins in **two different** `.env` files: `COMPOSE_PROFILES=html2docx` in the **repo-local** `.env` (starts the container) + `DJANGO_BPP_HTML2DOCX_URL=http://html2docx:3030/convert` in `$BPP_CONFIGS_DIR/.env` (points Django at it). Missing URL = soft degradation, not a crash. No published port, **no `docker.sock`** — removing the socket from `appserver` (which used to `docker run` the converter) was the whole point of the change. Image pinned by `HTML2DOCX_VERSION`, independent of `DOCKER_VERSION`. The old flag `DJANGO_BPP_ENABLE_HTML2DOCX_IMAGE` is **dead** — nothing reads it; `init-configs` deliberately neither writes nor strips it (harmless in old `.env`). Operator doc: `docs/konfiguracja/architektura.md`. ## Backwards Compatibility and `.env` Migrations — CRITICAL diff --git a/Makefile b/Makefile index e993801..b8118e5 100644 --- a/Makefile +++ b/Makefile @@ -192,6 +192,8 @@ help: @echo " init-configs - Re-initialize config directory structure" @echo " configure-resources - Tune Docker memory/CPU limits for this host" @echo " configure-rate-limits - Interaktywnie: limity ruchu nginx (globalne + spowalnianie per IP)" + @echo " configure - Interaktywnie: zmienne operatora (admin, SSL, powiadomienia, backup, Freshdesk...)" + @echo " config-check - Sprawdz poprawnosc zmiennych operatora w .env" @echo " create-monitoring-user - Create read-only bpp_monitor PG role (Grafana/Netdata)" @echo " grant-pg-monitor - (deprecated alias) -> create-monitoring-user" @echo " pg-monitoring-setup - slow-query log + pg_stat_statements + bpp_monitor" diff --git a/docs/eksploatacja/komendy.md b/docs/eksploatacja/komendy.md index 2268ce4..900252b 100644 --- a/docs/eksploatacja/komendy.md +++ b/docs/eksploatacja/komendy.md @@ -166,12 +166,15 @@ make update-ssl-certs # Przeładuj nginx po zmianie certyfikatów make init-configs # Uzupełnij brakujące pliki/zmienne (idempotentne) make configure-resources # Dostrój limity RAM/CPU make configure-rate-limits # Interaktywnie: limity ruchu nginx (globalne + spowalnianie per IP) +make configure # Interaktywnie: zmienne operatora (admin, SSL, powiadomienia, backup, Freshdesk) +make config-check # Sprawdź poprawność zmiennych operatora w .env (też w make doctor) +make docs-config-table # (dla deweloperów) przegeneruj tabelę w docs/konfiguracja/zmienne.md make generate-snakeoil-certs # Wygeneruj samopodpisane certyfikaty SSL make ssl-letsencrypt-issue # Wystaw cert Let's Encrypt (PROD=1 dla prawdziwego) make ssl-letsencrypt-renew # Manualny renew certów LE ``` -Szczegóły: [SSL](../konfiguracja/ssl.md), [Limity zasobów](../konfiguracja/limity-zasobow.md), [kreator limitów ruchu](../architektura/rate-limiting.md#kreator). +Szczegóły: [SSL](../konfiguracja/ssl.md), [Limity zasobów](../konfiguracja/limity-zasobow.md), [kreator limitów ruchu](../architektura/rate-limiting.md#kreator), [zmienne operatora](../konfiguracja/zmienne.md). ## Backup diff --git a/docs/konfiguracja/architektura.md b/docs/konfiguracja/architektura.md index e8e134e..94013fe 100644 --- a/docs/konfiguracja/architektura.md +++ b/docs/konfiguracja/architektura.md @@ -294,41 +294,14 @@ DJANGO_BPP_FRESHDESK_WIDGET_JWT_SECRET=wklej-tu-sekret-z-freshdeska Ustawiaj go tylko na serwerach, nad którymi masz pełną kontrolę (dostęp do `.env`). Nigdy nie commituj go do repozytorium. -### Kreator: `make configure-optional` i pytanie przy `make up` {#kreator-opcjonalnych} - -Tych wartości nie da się wygenerować ani zgadnąć, więc kreator po prostu o nie pyta: - -```bash -make configure-optional # pyta o wszystkie trzy; Enter = bez zmian, "-" = pusta -``` - -**`make up` (a także `make run`, `up-quick` i `refresh`) pyta sam**, jeśli którejś -z tych zmiennych **nie ma wcale** w `${BPP_CONFIGS_DIR}/.env`. Zwykle zdarza się to -raz, po `git pull` wydania, które je wprowadziło. **Pusta odpowiedź (Enter) też jest -odpowiedzią.** Zapisuje się wtedy `ZMIENNA=` (pusta wartość = funkcja wyłączona) -i przy następnym `make up` kreator już o nią nie pyta. Jeśli zmienisz zdanie, uruchom -`make configure-optional`. Wyjątek: przy ID widgetu dla zalogowanych Enter zapisuje -domyślne `205000000433`, czyli dotychczasowe działanie. - -Kreator **nie pyta**, gdy nie ma kogo pytać. Wtedy niczego nie zapisuje i `make up` -idzie dalej: - -- `make up` bez terminala (cron, CI, `| tee`): wypisuje tylko jedną linię z przypomnieniem, -- automatyczne aktualizacje i przerwa techniczna (`make autoupdate`, - `make run-with-warning`), a także `restore` i `upgrade-postgres`. Wszystkie te ścieżki - ustawiają `BPP_SKIP_HEALTH_GATE=1`. Pytania można też wyłączyć ręcznie przez - `BPP_NO_CONFIG_PROMPT=1 make up`, -- nikt nie odpowiada przez 120 s (`BPP_CONFIG_PROMPT_TIMEOUT`), np. przy `ssh -t` - bez człowieka po drugiej stronie. - -Sekret nie pojawia się na ekranie ani przy wpisywaniu, ani w podsumowaniu (kreator -pokazuje tylko jego długość). Kreator przyjmuje tylko litery, cyfry i `. _ ~ + / = -`. -`.env` czytają też Makefile i Compose, a dla nich `$` i `#` są składnią. - Przy włączonym JWT widget dla zalogowanych nie otwiera się bez tokenu. Zalogowany bez tokenu dostaje widget anonimowy, a gdy `..._ANON` jest pusty, zakładka kieruje do support.iplweb.pl. +Wartości najwygodniej ustawić kreatorem: `make up` sam zapyta o nie raz, a zmienić je można +przez `make configure` (grupa *Freshdesk*). Opis kreatora: +[Zmienne operatora](zmienne.md). + Szczegóły działania (endpoint `/freshdesk/widget-token/`, odnawianie tokenu, użytkownicy bez e-maila) opisuje dokumentacja BPP: *Administrator → Widget wsparcia Freshdesk*. Zmienne działają od wydania BPP zawierającego osobne widgety (PR iplweb/bpp#868). Na diff --git a/docs/konfiguracja/zmienne.md b/docs/konfiguracja/zmienne.md new file mode 100644 index 0000000..dd7c935 --- /dev/null +++ b/docs/konfiguracja/zmienne.md @@ -0,0 +1,137 @@ +# Zmienne operatora — `make configure` + +Większość zmiennych w `${BPP_CONFIGS_DIR}/.env` jest generowana albo ma rozsądną wartość +domyślną. Kilkanaście zależy jednak wyłącznie od Ciebie: e-mail administratora, tryb SSL, +powiadomienia, katalog backupów, widgety Freshdesk. Zebrane są w jednym rejestrze +(`scripts/config-registry.tsv`), z którego korzystają: + +- **`make init-configs`**: pyta o nie przy instalacji, +- **`make up`** (także `make run`, `up-quick`, `refresh`): pyta **raz** o te, których + w `.env` jeszcze nie ma, np. po aktualizacji, która dodała nową zmienną, +- **`make configure`**: menu grup do przejrzenia i zmiany wartości, +- **`make config-check`** i **`make doctor`** (pozycja *config*): sprawdzają, czy wartości są + poprawne (e-mail wygląda jak e-mail, ID widgetu jest liczbą itd.), +- tabela niżej na tej stronie, generowana z rejestru. + +## Kreator: `make configure` + +```bash +make configure +``` + +``` +=== Konfiguracja BPP — /home/bpp/publikacje-uczelnia/.env === + 1) Administrator (1) + 2) SSL (2) + 3) Powiadomienia (2) + 4) Google (2) + 5) Backup (3) + 6) Freshdesk (3) + z) zapisz i wyjdz q) wyjdz bez zapisu +Wybierz: +``` + +Po wyborze grupy kreator pyta po kolei o jej zmienne. **Enter** zostawia wartość +z nawiasu, **`-`** ustawia pustą (= funkcja wyłączona). Zmiany trafiają do `.env` +dopiero po **`z`**, a `q` je porzuca. Nowe wartości działają po `make up`. + +Sekrety (np. sekret JWT Freshdeska, token Rollbara) nie pojawiają się na ekranie ani +przy wpisywaniu, ani w podsumowaniu. Kreator pokazuje tylko ich długość. + +## Pytanie przy `make up` — „raz i już” {#pytanie-przy-make-up} + +Jeśli nowa wersja bpp-deploy wprowadza zmienną, o której musisz zdecydować (np. ID +widgetów Freshdesk), `make up` zatrzyma się przed startem kontenerów i zapyta o nią. + +**Pusta odpowiedź (Enter) też jest odpowiedzią.** Zapisuje się wtedy `ZMIENNA=` (pusta +wartość = funkcja wyłączona) i przy następnym `make up` kreator już o nią nie pyta. +O tym, że decyzja zapadła, świadczy **sama obecność zmiennej** w `.env`. Jeśli zmienisz +zdanie, uruchom `make configure`. + +Kreator **nie pyta**, gdy nie ma kogo pytać. Wtedy niczego nie zapisuje i `make up` +idzie dalej: + +- `make up` bez terminala (cron, CI, `| tee`): wypisuje tylko jedną linię z przypomnieniem, +- automatyczne aktualizacje i przerwa techniczna (`make autoupdate`, + `make run-with-warning`), a także `restore` i `upgrade-postgres`. Wszystkie te ścieżki + ustawiają `BPP_SKIP_HEALTH_GATE=1`. Pytania można też wyłączyć ręcznie przez + `BPP_NO_CONFIG_PROMPT=1 make up`, +- nikt nie odpowiada przez 120 s (`BPP_CONFIG_PROMPT_TIMEOUT`), np. przy `ssh -t` + bez człowieka po drugiej stronie. + +Przerwanie w połowie (Ctrl-D, timeout) **nie zapisuje niczego**, także odpowiedzi już +udzielonych, bo brak odpowiedzi nie jest decyzją. Kreator zapyta znowu przy następnym +`make up`. + +Zmienne z kolumną *Pytanie* = `nie` (np. tryb SSL, retencja backupów) mają wartość +domyślną, którą `init-configs` wpisuje bez pytania. `make up` o nie nie pyta, a zmienić je +można tylko w `make configure`. + +## Dozwolone znaki + +`.env` czytają trzy programy: Docker Compose (interpoluje `$`), `make` (dla niego `#` +to komentarz, a `$` to składnia) i skrypty. Dlatego kreator nie przyjmuje w wartościach +`$`, `#`, cudzysłowów, `\` ani `` ` ``. Sekrety mogą zawierać tylko litery, cyfry +i `. _ ~ + / = -`. + +## Lista zmiennych + + + +### Administrator + +| Zmienna | Typ | Domyślnie | Pytanie przy instalacji / `make up` | Może być pusta | Opis | +|---|---|---|---|---|---| +| `DJANGO_BPP_ADMIN_EMAIL` | email | `admin@example.com` | tak | nie | E-mail administratora BPP (konto admina, maile o bledach) | + +### SSL + +| Zmienna | Typ | Domyślnie | Pytanie przy instalacji / `make up` | Może być pusta | Opis | +|---|---|---|---|---|---| +| `DJANGO_BPP_SSL_MODE` | jedno z: manual/letsencrypt | `manual` | nie | nie | Skad nginx bierze certyfikat: manual = ssl/, letsencrypt = letsencrypt/live/<host>/ | +| `DJANGO_BPP_LETSENCRYPT_EMAIL` | email | = `DJANGO_BPP_ADMIN_EMAIL` | nie | nie | E-mail dla Let's Encrypt (powiadomienia o wygasajacych certyfikatach) | + +### Powiadomienia + +| Zmienna | Typ | Domyślnie | Pytanie przy instalacji / `make up` | Może być pusta | Opis | +|---|---|---|---|---|---| +| `DJANGO_BPP_SLACK_WEBHOOK` | url | *(pusta)* | tak | tak | Slack webhook URL (pusta = bez powiadomien na Slacka) | +| `ROLLBAR_ACCESS_TOKEN` | sekret | *(pusta)* | tak | tak | Rollbar access token typu post_server_item (pusta = backup-cycle bez powiadomien) | + +### Google + +| Zmienna | Typ | Domyślnie | Pytanie przy instalacji / `make up` | Może być pusta | Opis | +|---|---|---|---|---|---| +| `DJANGO_BPP_GOOGLE_ANALYTICS_PROPERTY_ID` | tekst | *(pusta)* | tak | tak | Google Analytics Property ID (pusta = bez GA) | +| `DJANGO_BPP_GOOGLE_VERIFICATION_CODE` | tekst | *(pusta)* | tak | tak | Google Search Console verification code (pusta = brak) | + +### Backup + +| Zmienna | Typ | Domyślnie | Pytanie przy instalacji / `make up` | Może być pusta | Opis | +|---|---|---|---|---|---| +| `DJANGO_BPP_HOST_BACKUP_DIR` | sciezka | `backups/` obok katalogu konfiguracyjnego | tak | nie | Katalog backupow na hoscie (sciezka bezwzgledna) | +| `DJANGO_BPP_BACKUP_KEEP_LAST` | liczba | `7` | nie | nie | Ile ostatnich kopii kazdego typu trzymac lokalnie | +| `DJANGO_BPP_RCLONE_REMOTE` | tekst | `backup_enc:` | nie | nie | Remote rclone dla kopii zdalnych (nazwa z rclone.conf zakonczona dwukropkiem) | + +### Freshdesk + +| Zmienna | Typ | Domyślnie | Pytanie przy instalacji / `make up` | Może być pusta | Opis | +|---|---|---|---|---|---| +| `DJANGO_BPP_FRESHDESK_WIDGET_ID` | id | `205000000433` | tak | tak | ID widgetu Freshdesk dla ZALOGOWANYCH (pusta = brak zakladki Pomoc dla zalogowanych) | +| `DJANGO_BPP_FRESHDESK_WIDGET_ID_ANON` | id | *(pusta)* | tak | tak | ID widgetu Freshdesk dla ANONIMOW (pusta = anonimowi nie widza zakladki Pomoc) | +| `DJANGO_BPP_FRESHDESK_WIDGET_JWT_SECRET` | sekret | *(pusta)* | tak | tak | Sekret JWT widgetu (Freshdesk: Admin > Account > Security > Widget settings; pusta = bez JWT) | + + + +Szczegóły poszczególnych funkcji: [SSL](ssl.md), +[backup i rclone](../eksploatacja/backup-i-rclone.md), +[widget Freshdesk](architektura.md#widget-wsparcia-freshdesk-django_bpp_freshdesk_widget_). + +## Dla deweloperów: dodanie zmiennej + +Nowa zmienna, o którą operator ma być zapytany, to **jeden wiersz** w +`scripts/config-registry.tsv` (format opisany w nagłówku pliku), a potem +`make docs-config-table`. Nie trzeba dopisywać `ensure_env_var` w `init-configs.sh` +ani migracji `.env`, bo brak zmiennej po prostu wywoła pytanie przy `make up`. Zmienna musi +mieć wartość domyślną w miejscu, które ją czyta (Compose `${VAR:-…}` albo ustawienia +Django), bo autoupdate i CI o nią nie zapytają. diff --git a/mk/configs.mk b/mk/configs.mk index 37419c1..80c4d2d 100644 --- a/mk/configs.mk +++ b/mk/configs.mk @@ -1,4 +1,4 @@ -.PHONY: update-ssl-certs generate-grafana-datasources update-configs configure-resources configure-rate-limits ensure-config-files zaspawaj-wersje +.PHONY: update-ssl-certs generate-grafana-datasources update-configs configure-resources configure-rate-limits configure ask-missing-config config-check docs-config-table 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 @@ -31,6 +31,28 @@ configure-resources: configure-rate-limits: @bash scripts/configure-rate-limits.sh +# Zmienne operatora z rejestru scripts/config-registry.tsv (e-mail admina, SSL, +# Slack/Rollbar, Google, backupy, Freshdesk). Menu grup, zapis na koncu. +configure: + @bash scripts/configure.sh + +# Prerequisit make up/up-quick/refresh: pyta TYLKO o zmienne z rejestru +# (PYTAJ=tak), ktorych w .env nie ma wcale. Pusta odpowiedz zapisuje `VAR=` +# = decyzja "nie mamy", wiec pytanie pada raz. Bez terminala / pod +# BPP_SKIP_HEALTH_GATE / BPP_NO_CONFIG_PROMPT nie pyta (autoupdate pod +# screenem ma pseudo-TTY). +ask-missing-config: + @bash scripts/configure.sh --missing + +# Walidacja wartosci z rejestru w .env (tez z make doctor). Exit 1 = bledy. +config-check: + @bash scripts/configure.sh --check + +# Przegenerowuje tabele zmiennych w docs/konfiguracja/zmienne.md z rejestru +# (miedzy znacznikami rejestr:start / rejestr:end). Test pilnuje aktualnosci. +docs-config-table: + @bash scripts/render-config-docs.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. diff --git a/mk/deployment.mk b/mk/deployment.mk index 1f58835..b5c069f 100644 --- a/mk/deployment.mk +++ b/mk/deployment.mk @@ -16,7 +16,7 @@ restart: validate-env-quotes update-configs docker compose down docker compose up -d -refresh: validate-env-quotes prune-orphan-volumes ensure-config-files update-configs +refresh: validate-env-quotes prune-orphan-volumes ask-missing-config ensure-config-files update-configs docker system prune -f docker compose pull docker compose stop @@ -32,7 +32,7 @@ pull: validate-env-quotes build: docker compose build -up: validate-env-quotes ensure-config-files update-configs +up: validate-env-quotes ask-missing-config ensure-config-files update-configs docker compose up -d --wait --force-recreate --remove-orphans @bash scripts/create-monitoring-user.sh --soft @# Stack jest UP i healthy (--wait powyzej; przy bledzie make juz by stanal). @@ -63,7 +63,7 @@ rmrf: @read -p "Are you sure? [y/N] " confirm && [ "$$confirm" = "y" ] || (echo "Aborted."; exit 1) docker compose rm -f -up-quick: validate-env-quotes ensure-config-files pull +up-quick: validate-env-quotes ask-missing-config ensure-config-files pull docker compose up -d --wait $(MAKE) invalidate diff --git a/mkdocs.yml b/mkdocs.yml index 6961ea6..8471b5c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -88,6 +88,7 @@ nav: - Pierwsze uruchomienie: instalacja/pierwsze-uruchomienie.md - Konfiguracja: - Architektura konfiguracji: konfiguracja/architektura.md + - Zmienne operatora (make configure): konfiguracja/zmienne.md - Struktura katalogów: konfiguracja/struktura-katalogow.md - SSL (manual / Let's Encrypt): konfiguracja/ssl.md - Multi-host: konfiguracja/multi-host.md diff --git a/scripts/config-registry.tsv b/scripts/config-registry.tsv new file mode 100644 index 0000000..2833048 --- /dev/null +++ b/scripts/config-registry.tsv @@ -0,0 +1,35 @@ +# Rejestr zmiennych .env, o ktorych decyduje OPERATOR (scripts/lib-config-registry.sh). +# Jedno zrodlo dla: init-configs (pytania przy instalacji), make up (pytanie +# raz o brakujace), make configure (menu grup), make config-check / doctor +# (walidacja) i tabeli w docs/konfiguracja/zmienne.md (make docs-config-table). +# +# Kolumny rozdzielone TABULATOREM: +# NAZWA zmienna w $BPP_CONFIGS_DIR/.env +# GRUPA naglowek w menu i w docs (kolejnosc grup = kolejnosc pierwszego wystapienia) +# TYP tekst | email | url | liczba | id | sciezka | sekret | wybor:a/b/... +# DOMYSLNA proponowana wartosc; ${NAZWA} = wartosc innej zmiennej z .env, +# ${BPP_DOMYSLNY_KATALOG_BACKUPOW} = /backups +# PYTAJ tak = pytamy, gdy zmiennej brak (init-configs, make up); +# nie = init-configs wpisuje DOMYSLNA bez pytania, edycja tylko w make configure +# PUSTA tak = pusta wartosc dozwolona (= funkcja wylaczona; tez jest decyzja) +# OPIS jedno zdanie (prompt + docs) +# +# Kolejnosc wierszy ma znaczenie: DOMYSLNA moze siegac tylko do zmiennych +# WYZEJ (LETSENCRYPT_EMAIL bierze ADMIN_EMAIL). +# NIE dopisuj tu zmiennych z wlasna logika (hostnames, baza, wersje PG): +# te zostaja w init-configs.sh. +# +# NAZWA GRUPA TYP DOMYSLNA PYTAJ PUSTA OPIS +DJANGO_BPP_ADMIN_EMAIL Administrator email admin@example.com tak nie E-mail administratora BPP (konto admina, maile o bledach) +DJANGO_BPP_SSL_MODE SSL wybor:manual/letsencrypt manual nie nie Skad nginx bierze certyfikat: manual = ssl/, letsencrypt = letsencrypt/live// +DJANGO_BPP_LETSENCRYPT_EMAIL SSL email ${DJANGO_BPP_ADMIN_EMAIL} nie nie E-mail dla Let's Encrypt (powiadomienia o wygasajacych certyfikatach) +DJANGO_BPP_SLACK_WEBHOOK Powiadomienia url tak tak Slack webhook URL (pusta = bez powiadomien na Slacka) +ROLLBAR_ACCESS_TOKEN Powiadomienia sekret tak tak Rollbar access token typu post_server_item (pusta = backup-cycle bez powiadomien) +DJANGO_BPP_GOOGLE_ANALYTICS_PROPERTY_ID Google tekst tak tak Google Analytics Property ID (pusta = bez GA) +DJANGO_BPP_GOOGLE_VERIFICATION_CODE Google tekst tak tak Google Search Console verification code (pusta = brak) +DJANGO_BPP_HOST_BACKUP_DIR Backup sciezka ${BPP_DOMYSLNY_KATALOG_BACKUPOW} tak nie Katalog backupow na hoscie (sciezka bezwzgledna) +DJANGO_BPP_BACKUP_KEEP_LAST Backup liczba 7 nie nie Ile ostatnich kopii kazdego typu trzymac lokalnie +DJANGO_BPP_RCLONE_REMOTE Backup tekst backup_enc: nie nie Remote rclone dla kopii zdalnych (nazwa z rclone.conf zakonczona dwukropkiem) +DJANGO_BPP_FRESHDESK_WIDGET_ID Freshdesk id 205000000433 tak tak ID widgetu Freshdesk dla ZALOGOWANYCH (pusta = brak zakladki Pomoc dla zalogowanych) +DJANGO_BPP_FRESHDESK_WIDGET_ID_ANON Freshdesk id tak tak ID widgetu Freshdesk dla ANONIMOW (pusta = anonimowi nie widza zakladki Pomoc) +DJANGO_BPP_FRESHDESK_WIDGET_JWT_SECRET Freshdesk sekret tak tak Sekret JWT widgetu (Freshdesk: Admin > Account > Security > Widget settings; pusta = bez JWT) diff --git a/scripts/configure.sh b/scripts/configure.sh new file mode 100644 index 0000000..c5414e0 --- /dev/null +++ b/scripts/configure.sh @@ -0,0 +1,307 @@ +#!/usr/bin/env bash +# ============================================================================ +# configure.sh — zmienne operatora z rejestru (scripts/config-registry.tsv) +# ============================================================================ +# Tryby: +# (bez argumentu) make configure — menu grup, edycja, zapis na koncu (z) +# --missing make up / up-quick / refresh — pyta TYLKO o zmienne +# z PYTAJ=tak, ktorych w .env nie ma wcale +# --init init-configs — jak --missing, ale koniec wejscia = +# wartosc domyslna (testy i instalacja z &2; exit 2 ;; +esac + +if [ "$TRYB" = brakujace ]; then + if [ -n "${BPP_SKIP_HEALTH_GATE:-}" ] || [ -n "${BPP_NO_CONFIG_PROMPT:-}" ]; then + exit 0 + fi +fi +INTERAKTYWNY=0 +if [ -t 0 ] || [ -n "${BPP_CO_ASSUME_TTY:-}" ]; then + INTERAKTYWNY=1 +fi + +# --- .env ------------------------------------------------------------------- +if [ -z "${BPP_CONFIGS_DIR:-}" ] && [ -f "$REPO_DIR/.env" ]; then + # shellcheck disable=SC1091 + . "$REPO_DIR/.env" +fi +if [ -z "${BPP_CONFIGS_DIR:-}" ] || [ ! -f "$BPP_CONFIGS_DIR/.env" ]; then + echo "BLAD: brak \$BPP_CONFIGS_DIR/.env. Uruchom najpierw 'make init-configs'." >&2 + exit 1 +fi +REJ_ENV="$BPP_CONFIGS_DIR/.env" +DOKUMENTACJA="https://iplweb.github.io/bpp-deploy/konfiguracja/zmienne/" + +# --- --check ------------------------------------------------------------------ +if [ "$TRYB" = sprawdz ]; then + bledy=0; brak=0 + while IFS=$'\037' read -r nazwa _grupa typ _dom pytaj pusta _opis; do + if ! rej_env_has "$nazwa"; then + if [ "$pytaj" = tak ]; then + echo " ? ${nazwa}: brak decyzji (zapytam przy make up albo: make configure)" + brak=$((brak + 1)) + fi + continue + fi + v="$(rej_env_get "$nazwa")" + if ! komunikat="$(rej_waliduj "$typ" "$pusta" "$v")"; then + echo " ✗ ${nazwa}=$(rej_pokaz "$typ" "$v"): ${komunikat}" + bledy=$((bledy + 1)) + fi + done < <(rej_wiersze) + if [ "$bledy" -gt 0 ]; then + echo "Zmienne operatora: ${bledy} z bledna wartoscia w ${REJ_ENV} — popraw: make configure" + exit 1 + fi + if [ "$brak" -gt 0 ]; then + echo "✓ Zmienne operatora poprawne (bez decyzji: ${brak})" + else + echo "✓ Zmienne operatora poprawne" + fi + exit 0 +fi + +# --- czytanie odpowiedzi -------------------------------------------------------- +TIMEOUT="${BPP_CONFIG_PROMPT_TIMEOUT:-120}" +case "$TIMEOUT" in ""|*[!0-9]*) TIMEOUT=120 ;; esac + +# czytaj UKRYJ -> $ODP (przyciete biale znaki); 1 = koniec wejscia / timeout. +czytaj() { + local x opcje=() + [ "$TRYB" = brakujace ] && opcje+=(-t "$TIMEOUT") + [ "$1" = 1 ] && [ -t 0 ] && opcje+=(-s) + # ${a[@]+...}: pusta tablica pod set -u wywraca bash 3.2 (macOS) + if ! IFS= read -r ${opcje[@]+"${opcje[@]}"} x; then + echo "" + return 1 + fi + [ "$1" = 1 ] && [ -t 0 ] && echo "" + x="${x%$'\r'}" + x="${x#"${x%%[![:space:]]*}"}"; x="${x%"${x##*[![:space:]]}"}" + ODP="$x" +} + +# zapytaj TYP PUSTA OPIS OBECNA -> $NOWA. Enter = OBECNA, '-' = pusta. +# 1 = koniec wejscia (w trybie init: NOWA=OBECNA i 0). +zapytaj() { + local typ="$1" pusta="$2" opis="$3" obecna="$4" komunikat ukryj=0 + [ "$typ" = sekret ] && ukryj=1 + echo " ${opis}" + while true; do + printf ' > [%s] ' "$(rej_pokaz "$typ" "$obecna")" + if ! czytaj "$ukryj"; then + if [ "$TRYB" = init ]; then NOWA="$obecna"; return 0; fi + return 1 + fi + case "$ODP" in + "") NOWA="$obecna" ;; + -) NOWA="" ;; + *) NOWA="$ODP" ;; + esac + if komunikat="$(rej_waliduj "$typ" "$pusta" "$NOWA")"; then + return 0 + fi + echo " ${komunikat}" + done +} + +# Zmiany do zapisu: rownolegle tablice (bez tablic asocjacyjnych — bash 3.2). +declare -a Z_NAZWA=() Z_WARTOSC=() Z_TYP=() Z_OPIS=() +dodaj_zmiane() { # NAZWA WARTOSC TYP GRUPA: OPIS + local i + for i in ${Z_NAZWA[@]+"${!Z_NAZWA[@]}"}; do + if [ "${Z_NAZWA[$i]}" = "$1" ]; then + Z_WARTOSC[i]="$2"; return + fi + done + Z_NAZWA+=("$1"); Z_WARTOSC+=("$2"); Z_TYP+=("$3"); Z_OPIS+=("$4") +} + +# Dla ${NAZWA} w DOMYSLNA: oczekujaca zmiana przed .env. +rej_odczyt() { + local i + for i in ${Z_NAZWA[@]+"${!Z_NAZWA[@]}"}; do + [ "${Z_NAZWA[$i]}" = "$1" ] && { printf '%s' "${Z_WARTOSC[$i]}"; return; } + done + rej_env_get "$1" +} + +# Wartosc biezaca: oczekujaca zmiana > .env > domyslna z rejestru. +biezaca() { # NAZWA DOMYSLNA_SUROWA + local i + for i in ${Z_NAZWA[@]+"${!Z_NAZWA[@]}"}; do + [ "${Z_NAZWA[$i]}" = "$1" ] && { printf '%s' "${Z_WARTOSC[$i]}"; return; } + done + if rej_env_has "$1"; then rej_env_get "$1"; else rej_domyslna "$2"; fi +} + +zapisz() { + local i zmienione=0 + for i in ${Z_NAZWA[@]+"${!Z_NAZWA[@]}"}; do + if rej_env_has "${Z_NAZWA[$i]}" && [ "$(rej_env_get "${Z_NAZWA[$i]}")" = "${Z_WARTOSC[$i]}" ]; then + continue + fi + if rej_env_has "${Z_NAZWA[$i]}"; then + echo " ~ ${Z_NAZWA[$i]}=$(rej_pokaz "${Z_TYP[$i]}" "${Z_WARTOSC[$i]}")" + else + echo " + ${Z_NAZWA[$i]}=$(rej_pokaz "${Z_TYP[$i]}" "${Z_WARTOSC[$i]}")" + fi + rej_env_set "${Z_NAZWA[$i]}" "${Z_WARTOSC[$i]}" "${Z_OPIS[$i]}" + zmienione=1 + done + [ "$zmienione" = 1 ] +} + +# --- --missing / --init ---------------------------------------------------------- +# Wiersze rejestru czytamy z deskryptora 3 — stdin jest na odpowiedzi. +if [ "$TRYB" = brakujace ] || [ "$TRYB" = init ]; then + WIERSZE="$(rej_wiersze)" + ile=0 + while IFS=$'\037' read -r nazwa _g _t _d pytaj _p _o <&3; do + if [ "$pytaj" = tak ] && ! rej_env_has "$nazwa"; then ile=$((ile + 1)); fi + done 3<<<"$WIERSZE" + + if [ "$TRYB" = brakujace ]; then + [ "$ile" -eq 0 ] && exit 0 + if [ "$INTERAKTYWNY" = 0 ]; then + echo "INFO: zmienne operatora bez decyzji (${ile}) — uzupelnij: make configure" + exit 0 + fi + echo "" + echo "================================================================" + echo " Nowe ustawienia BPP (${ile}) — pytam raz, potem juz nie" + echo "================================================================" + echo "Enter = wartosc w nawiasie, '-' = pusta (wylaczone)." + echo "Pusta odpowiedz to tez decyzja ('nie mamy tego') — zmiana pozniej: make configure" + echo "Opis zmiennych: $DOKUMENTACJA" + fi + + while IFS=$'\037' read -r nazwa grupa typ dom pytaj pusta opis <&3; do + rej_env_has "$nazwa" && continue + wartosc="$(biezaca "$nazwa" "$dom")" + if [ "$pytaj" = tak ]; then + echo "" + echo "${nazwa}" + if ! zapytaj "$typ" "$pusta" "$opis" "$wartosc"; then + echo "Brak odpowiedzi — nic nie zapisano (zapytam ponownie przy nastepnym make up)." + exit 0 + fi + wartosc="$NOWA" + elif [ "$TRYB" = brakujace ]; then + continue # PYTAJ=nie: make up nie dopisuje (domyslna zyje w kodzie) + fi + dodaj_zmiane "$nazwa" "$wartosc" "$typ" "${grupa}: ${opis}" + done 3<<<"$WIERSZE" + + echo "" + if zapisz; then + echo "Zapisano w ${REJ_ENV}." + elif [ "$TRYB" = brakujace ]; then + echo "Bez zmian." + fi + exit 0 +fi + +# --- menu (make configure) ------------------------------------------------------- +if [ "$INTERAKTYWNY" = 0 ]; then + echo "BLAD: make configure jest interaktywne — uruchom w terminalu." >&2 + exit 1 +fi + +GRUPY="$(rej_grupy)" + +edytuj_grupe() { # GRUPA + local nazwa grupa typ dom pytaj pusta opis wartosc + echo "" + echo "== $1 == (Enter = bez zmian, '-' = pusta)" + while IFS=$'\037' read -r nazwa grupa typ dom pytaj pusta opis <&3; do + [ "$grupa" = "$1" ] || continue + wartosc="$(biezaca "$nazwa" "$dom")" + echo "" + echo "${nazwa}$(rej_env_has "$nazwa" || printf ' (brak w .env, proponuje domyslna)')" + if ! zapytaj "$typ" "$pusta" "$opis" "$wartosc"; then + return 1 + fi + # Bez zmiany i juz w .env -> nic do zapisu; brak w .env + Enter -> zapis decyzji. + if rej_env_has "$nazwa" && [ "$NOWA" = "$(rej_env_get "$nazwa")" ]; then + continue + fi + dodaj_zmiane "$nazwa" "$NOWA" "$typ" "${grupa}: ${opis}" + done 3<<<"$(rej_wiersze)" +} + +while true; do + echo "" + echo "=== Konfiguracja BPP — ${REJ_ENV} ===" + n=0 + while IFS= read -r g <&3; do + n=$((n + 1)) + ile_g="$(rej_wiersze | awk -F'\037' -v g="$g" '$2 == g' | grep -c .)" + printf ' %d) %-15s (%d)\n' "$n" "$g" "$ile_g" + done 3<<<"$GRUPY" + echo " z) zapisz i wyjdz q) wyjdz bez zapisu" + if [ "${#Z_NAZWA[@]}" -gt 0 ]; then + echo " Niezapisane zmiany: ${#Z_NAZWA[@]}" + fi + printf 'Wybierz: ' + if ! czytaj 0; then + echo "Koniec wejscia — nic nie zapisano." + exit 1 + fi + case "$ODP" in + z|Z) + echo "" + if zapisz; then + echo "Zapisano w ${REJ_ENV}. Nowe wartosci zadzialaja po: make up" + else + echo "Bez zmian." + fi + exit 0 ;; + q|Q) + [ "${#Z_NAZWA[@]}" -gt 0 ] && echo "Porzucono niezapisane zmiany: ${#Z_NAZWA[@]}" + exit 0 ;; + "") ;; + *[!0-9]*) echo "Nieznany wybor: $ODP" ;; + *) + g="$(printf '%s\n' "$GRUPY" | sed -n "${ODP}p")" + if [ -z "$g" ]; then + echo "Nieznany wybor: $ODP" + elif ! edytuj_grupe "$g"; then + echo "Koniec wejscia — nic nie zapisano." + exit 1 + fi ;; + esac +done diff --git a/scripts/doctor.sh b/scripts/doctor.sh index 7171f8a..9ce1b97 100755 --- a/scripts/doctor.sh +++ b/scripts/doctor.sh @@ -12,7 +12,7 @@ # Uruchomienie: # make doctor # interaktywne menu # bash scripts/doctor.sh # j.w. -# bash scripts/doctor.sh mail # nieinteraktywnie: mail|ntfy|rollbar|health|backup|all +# bash scripts/doctor.sh mail # nieinteraktywnie: mail|ntfy|rollbar|health|backup|config|all # # Uwaga: NIE ustawiamy `-e` - zawodzacy pojedynczy test (np. brak env) ma wrocic # do menu, a nie ubic calego doctora. @@ -38,6 +38,7 @@ run_choice() { rollbar) run_target test-rollbar ;; health) run_target health ;; backup) run_target backup-cycle ;; + config) run_target config-check ;; all) # "wszystko" = trio powiadomien (dawne post-deploy zachowanie, # ale na zadanie). Swiadomie BEZ health/backup. @@ -47,7 +48,7 @@ run_choice() { ;; *) echo "BLAD: nieznana pozycja '$1'" >&2 - echo "Dozwolone: mail ntfy rollbar health backup all" >&2 + echo "Dozwolone: mail ntfy rollbar health backup config all" >&2 return 2 ;; esac @@ -63,6 +64,7 @@ print_menu() { 4) health — status usług + ostatnie błędy (health) 5) backup — pełny cykl backupu (backup-cycle) 6) wszystko — mail + ntfy + rollbar po kolei + 7) config — poprawność zmiennych operatora (config-check) q) wyjście EOF } @@ -83,6 +85,7 @@ menu() { 4|health) run_choice health || true ;; 5|backup) run_choice backup || true ;; 6|all|wszystko) run_choice all || true ;; + 7|config) run_choice config || true ;; q|Q|quit|exit) break ;; "") : ;; # pusty Enter -> przerysuj menu *) echo "Nieznany wybór: $choice" >&2 ;; diff --git a/scripts/init-configs.sh b/scripts/init-configs.sh index 4f03d4a..3a8bc5f 100755 --- a/scripts/init-configs.sh +++ b/scripts/init-configs.sh @@ -228,23 +228,8 @@ if [ ! -f "$ENV_FILE" ]; then read -r ADMIN_USERNAME || true ADMIN_USERNAME="${ADMIN_USERNAME:-admin}" - printf "Email administratora [admin@example.com]: " - read -r ADMIN_EMAIL || true - ADMIN_EMAIL="${ADMIN_EMAIL:-admin@example.com}" - - printf "Slack webhook URL (opcjonalny, Enter = pomin): " - read -r SLACK_WEBHOOK || true - - DEFAULT_BACKUP_DIR="$(dirname "$ABS_CONFIG")/backups" - printf "Katalog backupow [%s]: " "$DEFAULT_BACKUP_DIR" - read -r BACKUP_DIR || true - BACKUP_DIR="${BACKUP_DIR:-$DEFAULT_BACKUP_DIR}" - - printf "Google Analytics Property ID (opcjonalny, Enter = pomin): " - read -r GA_PROPERTY_ID || true - - printf "Google Verification Code (opcjonalny, Enter = pomin): " - read -r GA_VERIFICATION_CODE || true + # E-mail admina, Slack, katalog backupow, Google, Freshdesk... pyta + # rejestr (scripts/config-registry.tsv) nizej, po wygenerowaniu pliku. SECRET_KEY="$(openssl rand -base64 48 | head -c 50)" @@ -342,44 +327,14 @@ STATIC_ROOT=/staticroot/ DJANGO_BPP_MEDIA_ROOT=/mediaroot DJANGO_SETTINGS_MODULE=django_bpp.settings.production -# === Google (opcjonalne) === -DJANGO_BPP_GOOGLE_ANALYTICS_PROPERTY_ID=$GA_PROPERTY_ID -DJANGO_BPP_GOOGLE_VERIFICATION_CODE=$GA_VERIFICATION_CODE - # === Bezpieczenstwo === DJANGO_BPP_SECRET_KEY=$SECRET_KEY # === Administrator === DJANGO_BPP_ADMIN_USERNAME=$ADMIN_USERNAME -DJANGO_BPP_ADMIN_EMAIL=$ADMIN_EMAIL -ADMINS=$ADMIN_USERNAME <$ADMIN_EMAIL> - -# === SSL === -# DJANGO_BPP_SSL_MODE: ktore certyfikaty serwuje nginx. -# manual - czyta z \$BPP_CONFIGS_DIR/ssl/ (snakeoil albo wgrane recznie). -# letsencrypt - czyta z \$BPP_CONFIGS_DIR/letsencrypt/live//. Wystaw -# cert przez 'make ssl-letsencrypt-issue PROD=1'. Codzienny -# renew dziala automatycznie przez Ofelia. -# Manualne certy sa zachowane fizycznie nawet przy mode=letsencrypt - LE pisze -# do osobnego katalogu, mozna w kazdej chwili przelaczyc tryb tam i z powrotem. -DJANGO_BPP_SSL_MODE=manual -# Email dla Let's Encrypt (powiadomienia o wygasajacych certach). -# Domyslnie = DJANGO_BPP_ADMIN_EMAIL. Mozesz nadpisac jesli LE notyfikacje -# maja chodzic na inny adres niz administrator BPP. -DJANGO_BPP_LETSENCRYPT_EMAIL=$ADMIN_EMAIL - -# === Powiadomienia (opcjonalne) === -DJANGO_BPP_SLACK_WEBHOOK=$SLACK_WEBHOOK - -# === Backupy === -DJANGO_BPP_HOST_BACKUP_DIR=$BACKUP_DIR -DJANGO_BPP_BACKUP_KEEP_LAST=7 -DJANGO_BPP_RCLONE_REMOTE=backup_enc: - -# === Rollbar (opcjonalne, dla notyfikacji backup-cycle) === -# Gdy puste, backup-cycle.sh pomija powiadomienia. Uzyj tokena typu -# "post_server_item" z https://rollbar.com//settings/access_tokens/ -ROLLBAR_ACCESS_TOKEN= +# E-mail admina (i ADMINS), SSL, powiadomienia (Slack, Rollbar), Google, +# backupy i Freshdesk: na koncu pliku, dopisane z rejestru zmiennych +# operatora (scripts/config-registry.tsv). Zmiana: make configure. # === Docker logging (driver "local", binarny, skompresowany zstd) === # Rotacja na poziomie hosta. Czasowa retencja zyje w Loki @@ -456,6 +411,10 @@ EOF ensure_env_var "DJANGO_BPP_NTFY_SERVER" "https://ntfy.sh" "" \ "Serwer ntfy do alertow (default publiczny ntfy.sh)" + BPP_CONFIGS_DIR="$ABS_CONFIG" bash "$REPO_DIR/scripts/configure.sh" --init + _email="$(get_env_var DJANGO_BPP_ADMIN_EMAIL)" + ensure_env_var "ADMINS" "$ADMIN_USERNAME <$_email>" "" "Administrator (format Django)" + echo "Wygenerowano $ENV_FILE z losowymi haslami." else @@ -465,7 +424,6 @@ else echo "" DEFAULT_HOSTNAME="$(hostname -f 2>/dev/null || hostname)" - DEFAULT_BACKUP_DIR="$(dirname "$ABS_CONFIG")/backups" # W trybie zewnetrznej bazy - zapytaj o parametry polaczenia i nadpisz je # w .env (nawet jesli juz byly ustawione na stare wartosci typu "dbserver"). @@ -571,40 +529,12 @@ else ensure_env_var "DJANGO_BPP_MEDIA_ROOT" "/mediaroot" "" "Pliki media (upload)" ensure_env_var "DJANGO_SETTINGS_MODULE" \ "django_bpp.settings.production" "" "Django settings" - ensure_env_var "DJANGO_BPP_GOOGLE_ANALYTICS_PROPERTY_ID" "" \ - "Google Analytics Property ID (opcjonalny, Enter = pomin)" \ - "Google Analytics (opcjonalne)" - ensure_env_var "DJANGO_BPP_GOOGLE_VERIFICATION_CODE" "" \ - "Google Verification Code (opcjonalny, Enter = pomin)" \ - "Google Verification (opcjonalne)" ensure_env_var "DJANGO_BPP_SECRET_KEY" \ "$(openssl rand -base64 48 | head -c 50)" "" "Bezpieczenstwo" ensure_env_var "DJANGO_BPP_ADMIN_USERNAME" "admin" \ "Nazwa uzytkownika administratora" "Administrator" - ensure_env_var "DJANGO_BPP_ADMIN_EMAIL" "admin@example.com" \ - "Email administratora" "Administrator" - - # ADMINS wymaga username + email — budujemy z istniejących wartości - if ! env_has_var "ADMINS"; then - # Odczytaj wartości które właśnie dopisaliśmy lub były wcześniej - _username=$(grep "^DJANGO_BPP_ADMIN_USERNAME=" "$ENV_FILE" | head -1 | cut -d= -f2-) - _email=$(grep "^DJANGO_BPP_ADMIN_EMAIL=" "$ENV_FILE" | head -1 | cut -d= -f2-) - ensure_env_var "ADMINS" "$_username <$_email>" "" "Administrator (format Django)" - fi - - # SSL mode + Let's Encrypt email - dodajemy z domyslnymi wartosciami - # bezpiecznymi dla istniejacych deploymentow (mode=manual = obecne zachowanie). - # Email dziedziczy z DJANGO_BPP_ADMIN_EMAIL zeby uniknac drugiego prompta. - ensure_env_var "DJANGO_BPP_SSL_MODE" "manual" "" \ - "SSL: 'manual' (czyta ssl/) lub 'letsencrypt' (czyta letsencrypt/live//)" - _admin_email_for_le="$(get_env_var DJANGO_BPP_ADMIN_EMAIL)" - ensure_env_var "DJANGO_BPP_LETSENCRYPT_EMAIL" "${_admin_email_for_le:-admin@example.com}" "" \ - "Email dla Let's Encrypt (default = DJANGO_BPP_ADMIN_EMAIL)" - - ensure_env_var "DJANGO_BPP_SLACK_WEBHOOK" "" \ - "Slack webhook URL (opcjonalny, Enter = pomin)" "Powiadomienia (opcjonalne)" # Migracja: stara nazwa zmiennej DJANGO_BPP_BACKUP_DIR -> nowa # DJANGO_BPP_HOST_BACKUP_DIR (dodana gdy dbserver dostal bind-mount # /backup, zeby nazwa podkreslala ze to katalog na hoscie). @@ -617,15 +547,18 @@ else echo " ~ zmigrowalem DJANGO_BPP_BACKUP_DIR -> DJANGO_BPP_HOST_BACKUP_DIR" fi - ensure_env_var "DJANGO_BPP_HOST_BACKUP_DIR" "$DEFAULT_BACKUP_DIR" \ - "Katalog backupow na hoscie" "Backupy" - ensure_env_var "DJANGO_BPP_BACKUP_KEEP_LAST" "7" \ - "" "Backupy - ile ostatnich kopii kazdego typu trzymac lokalnie" - ensure_env_var "DJANGO_BPP_RCLONE_REMOTE" "backup_enc:" \ - "" "Backupy - rclone remote (np. backup_enc:)" - ensure_env_var "ROLLBAR_ACCESS_TOKEN" "" \ - "Rollbar access token (post_server_item, opcjonalny, Enter = pomin)" \ - "Rollbar (notyfikacje backup-cycle)" + # Zmienne operatora (e-mail admina, SSL, Slack, Rollbar, Google, backupy, + # Freshdesk): jedno zrodlo = scripts/config-registry.tsv. PO migracji + # DJANGO_BPP_BACKUP_DIR wyzej, inaczej rejestr zapytalby o katalog + # backupow mimo starej wartosci. + BPP_CONFIGS_DIR="$ABS_CONFIG" bash "$REPO_DIR/scripts/configure.sh" --init + + # ADMINS wymaga username + email — budujemy z istniejących wartości + if ! env_has_var "ADMINS"; then + _username="$(get_env_var DJANGO_BPP_ADMIN_USERNAME)" + _email="$(get_env_var DJANGO_BPP_ADMIN_EMAIL)" + ensure_env_var "ADMINS" "$_username <$_email>" "" "Administrator (format Django)" + fi ensure_env_var "LOG_MAX_SIZE" "150m" "" \ "Docker log rotation - rozmiar jednego pliku (log driver=local, zstd)" diff --git a/scripts/lib-config-registry.sh b/scripts/lib-config-registry.sh new file mode 100644 index 0000000..59f154e --- /dev/null +++ b/scripts/lib-config-registry.sh @@ -0,0 +1,188 @@ +# shellcheck shell=bash +# ============================================================================ +# lib-config-registry.sh — odczyt rejestru zmiennych operatora +# (scripts/config-registry.tsv), walidacja i zapis do .env. +# ============================================================================ +# Uzywane przez scripts/configure.sh (make configure / make up / init-configs / +# make config-check / make docs-config-table). Prefiks rej_ — biblioteka bywa +# source'owana obok helperow init-configs (env_has_var, set_env_var...), +# nie wolno ich przeslonic. +# +# Wymaga: REJ_ENV = sciezka do $BPP_CONFIGS_DIR/.env (dla rej_env_*). +# Bez tablic asocjacyjnych — bash 3.2 (macOS) ich nie ma. +# ============================================================================ + +REJ_PLIK="${REJ_PLIK:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/config-registry.tsv}" + +# Wiersze rejestru bez komentarzy i pustych linii, pola rozdzielone \037 +# (US), NIE tabulatorem: `read` z IFS=$'\t' skleja dwa tabulatory pod rzad +# (tab to "bialy" IFS), wiec pusta kolumna DOMYSLNA przesuwalaby pola. +# Czytaj: while IFS=$'\037' read -r nazwa grupa typ dom pytaj pusta opis +rej_wiersze() { + awk -F'\t' -v OFS='\037' '!/^#/ && NF == 7 { $1 = $1; print }' "$REJ_PLIK" +} + +# Grupy w kolejnosci pierwszego wystapienia, po jednej w linii. +rej_grupy() { + rej_wiersze | awk -F'\037' '!seen[$2]++ { print $2 }' +} + +# --- .env --------------------------------------------------------------------- +rej_env_has() { + grep -q "^${1}=" "$REJ_ENV" 2>/dev/null +} + +rej_env_get() { + local raw + raw="$(grep -E "^${1}=" "$REJ_ENV" 2>/dev/null | tail -1 | cut -d= -f2-)" || true + raw="${raw%$'\r'}" + raw="${raw#\"}"; raw="${raw%\"}" + raw="${raw#\'}"; raw="${raw%\'}" + printf '%s' "$raw" +} + +# rej_env_set NAZWA WARTOSC [KOMENTARZ] — podmiana w miejscu albo dopisanie +# na koncu (z komentarzem nad zmienna). Wartosc przez ENVIRON, nie `awk -v` +# (-v interpretuje backslashe). Plik tymczasowy z mktemp (0600, w .env sa +# hasla) i `cat >` zamiast `mv` — zostaje tryb i wlasciciel .env. +rej_env_set() { + local nazwa="$1" wartosc="$2" komentarz="${3:-}" tmp + if rej_env_has "$nazwa"; then + tmp="$(mktemp "$REJ_ENV.tmp.XXXXXX")" + K="$nazwa" V="$wartosc" awk ' + BEGIN { k = ENVIRON["K"]; v = ENVIRON["V"] } + index($0, k "=") == 1 { print k "=" v; next } + { print } + ' "$REJ_ENV" > "$tmp" + cat "$tmp" > "$REJ_ENV" + rm -f "$tmp" + else + # .env bez konczacego \n skleilby nasza linie z ostatnia + if [ -s "$REJ_ENV" ] && [ -n "$(tail -c1 "$REJ_ENV")" ]; then + echo >> "$REJ_ENV" + fi + { + echo "" + [ -n "$komentarz" ] && echo "# $komentarz" + printf '%s=%s\n' "$nazwa" "$wartosc" + } >> "$REJ_ENV" + fi +} + +# Wartosc innej zmiennej dla ${NAZWA} w DOMYSLNA. configure.sh przeslania ja, +# zeby widziec tez jeszcze niezapisane odpowiedzi (LETSENCRYPT_EMAIL <- ADMIN_EMAIL). +rej_odczyt() { + rej_env_get "$1" +} + +# --- wartosci ----------------------------------------------------------------- +# Rozwija ${NAZWA} w kolumnie DOMYSLNA: wartosc z .env albo wyliczona. +rej_domyslna() { + local s="$1" wynik="" przed nazwa wartosc + while :; do + # shellcheck disable=SC2016 # dosłowne ${...}, nie rozwijamy + case "$s" in + *'${'*'}'*) ;; + *) break ;; + esac + przed="${s%%\$\{*}" + s="${s#*\$\{}" + nazwa="${s%%\}*}" + s="${s#*\}}" + case "$nazwa" in + BPP_DOMYSLNY_KATALOG_BACKUPOW) + wartosc="$(dirname "${BPP_CONFIGS_DIR:-$(dirname "$REJ_ENV")}")/backups" ;; + *) wartosc="$(rej_odczyt "$nazwa")" ;; + esac + wynik="$wynik$przed$wartosc" + done + printf '%s' "$wynik$s" +} + +# Jak wartosc pokazac na ekranie / w podsumowaniu (sekretow nie pokazujemy). +rej_pokaz() { # TYP WARTOSC + if [ "$1" = sekret ]; then + if [ -n "$2" ]; then printf '(ustawiony, %s znakow)' "${#2}"; else printf '(pusty)'; fi + else + if [ -n "$2" ]; then printf '%s' "$2"; else printf '(pusta)'; fi + fi +} + +# rej_waliduj TYP PUSTA WARTOSC — 0 = OK; inaczej komunikat na stdout, 1. +# Zakazane wszedzie: `$` i `#` (.env jest tez `-include` w Makefile, a Compose +# interpoluje `$`), cudzyslowy (Compose przekazuje je literalnie — +# validate-env-quotes.sh), backslash i backtick. +rej_waliduj() { + local typ="$1" pusta="$2" v="$3" + if [ -z "$v" ]; then + [ "$pusta" = tak ] && return 0 + echo "wartosc jest wymagana"; return 1 + fi + # shellcheck disable=SC1003 # '\' to dosłowny backslash + case "$v" in + *'$'*|*'#'*|*'"'*|*"'"*|*'\'*|*'`'*) + echo "niedozwolony znak (\$ # cudzyslow \\ \`) — psuje .env czytany przez make i Compose"; return 1 ;; + esac + case "$typ" in + email) + if ! printf '%s' "$v" | grep -Eq '^[^@[:space:]]+@[^@[:space:]]+\.[^@[:space:]]+$'; then + echo "to nie wyglada na adres e-mail"; return 1 + fi ;; + url) + case "$v" in + http://*|https://*) ;; + *) echo "URL musi zaczynac sie od https:// (albo http://)"; return 1 ;; + esac + case "$v" in *[[:space:]]*) echo "URL nie moze zawierac spacji"; return 1 ;; esac ;; + liczba|id) + case "$v" in *[!0-9]*) + if [ "$typ" = id ]; then + echo "ID widgetu to liczba (z kodu osadzenia: 'widget_id': 205000000433)" + else + echo "podaj liczbe calkowita >= 0" + fi + return 1 ;; + esac ;; + sciezka) + case "$v" in + /*) ;; + *) echo "podaj sciezke bezwzgledna (od /)"; return 1 ;; + esac ;; + sekret) + case "$v" in *[!A-Za-z0-9._~+/=-]*) + echo "sekret moze zawierac tylko litery, cyfry i . _ ~ + / = -"; return 1 ;; + esac ;; + wybor:*) + case "/${typ#wybor:}/" in + *"/$v/"*) ;; + *) echo "dozwolone: ${typ#wybor:}"; return 1 ;; + esac ;; + tekst) + case "$v" in [[:space:]]*|*[[:space:]]) echo "bez spacji na poczatku i koncu"; return 1 ;; esac ;; + *) + echo "nieznany typ w rejestrze: $typ"; return 1 ;; + esac + return 0 +} + +# Tabela Markdown dla docs/ (make docs-config-table). +rej_docs() { + rej_wiersze | awk -F'\037' ' + function md(s) { gsub(/\|/, "\\|", s); gsub(//, "\\>", s); return s } + { + if ($2 != grupa) { + if (grupa != "") print "" + grupa = $2 + print "### " grupa + print "" + print "| Zmienna | Typ | Domyślnie | Pytanie przy instalacji / `make up` | Może być pusta | Opis |" + print "|---|---|---|---|---|---|" + } + if ($4 == "") d = "*(pusta)*" + else if ($4 == "${BPP_DOMYSLNY_KATALOG_BACKUPOW}") d = "`backups/` obok katalogu konfiguracyjnego" + else if ($4 ~ /^\$\{[A-Z_0-9]+\}$/) { d = $4; gsub(/[${}]/, "", d); d = "= `" d "`" } + else d = "`" $4 "`" + typ = $3; sub(/^wybor:/, "jedno z: ", typ) + print "| `" $1 "` | " md(typ) " | " md(d) " | " $5 " | " $6 " | " md($7) " |" + }' +} diff --git a/scripts/render-config-docs.sh b/scripts/render-config-docs.sh new file mode 100644 index 0000000..f823758 --- /dev/null +++ b/scripts/render-config-docs.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Wstawia tabele zmiennych operatora (z scripts/config-registry.tsv) do +# docs/konfiguracja/zmienne.md, miedzy znaczniki rejestr:start / rejestr:end. +# render-config-docs.sh przepisuje plik (make docs-config-table) +# render-config-docs.sh --check exit 1, gdy tabela w docs jest nieaktualna +# (test_configure_registry w tests/test_makefile.sh) +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" +DOC="${BPP_CONFIG_DOC:-$REPO_DIR/docs/konfiguracja/zmienne.md}" +START='' +END='' + +for z in "$START" "$END"; do + if [ "$(grep -cxF -- "$z" "$DOC")" != 1 ]; then + echo "BLAD: w $DOC musi byc dokladnie jeden znacznik: $z" >&2 + exit 1 + fi +done + +TABELA="$(bash "$REPO_DIR/scripts/configure.sh" --docs)" +NOWY="$(START="$START" END="$END" TABELA="$TABELA" awk ' + $0 == ENVIRON["START"] { print; print ""; print ENVIRON["TABELA"]; print ""; pomin = 1; next } + $0 == ENVIRON["END"] { pomin = 0 } + !pomin { print } +' "$DOC")" + +if [ "${1:-}" = --check ]; then + if [ "$NOWY" != "$(cat "$DOC")" ]; then + echo "Tabela zmiennych w $DOC jest nieaktualna wzgledem scripts/config-registry.tsv — uruchom: make docs-config-table" >&2 + exit 1 + fi + exit 0 +fi +printf '%s\n' "$NOWY" > "$DOC" +echo "Zaktualizowano tabele w $DOC" diff --git a/scripts/test-doctor.sh b/scripts/test-doctor.sh index d6e5972..992af94 100755 --- a/scripts/test-doctor.sh +++ b/scripts/test-doctor.sh @@ -116,6 +116,10 @@ run_doctor backup assert_log_has "backup-cycle" "backup -> make backup-cycle" assert_log_lines 1 "backup: dokladnie jeden cel" +run_doctor config +assert_log_has "config-check" "config -> make config-check" +assert_log_lines 1 "config: dokladnie jeden cel" + # "wszystko" = mail + ntfy + rollbar (BEZ health/backup). run_doctor all assert_exit 0 "$RUN_EXIT" "all: exit 0" @@ -123,8 +127,8 @@ assert_log_has "test-email" "all zawiera test-email" assert_log_has "test-ntfy" "all zawiera test-ntfy" assert_log_has "test-rollbar" "all zawiera test-rollbar" assert_log_lines 3 "all: dokladnie trzy cele" -if grep -qE 'health|backup-cycle' "$MAKE_LOG"; then - fail "all: NIE powinno zawierac health/backup" +if grep -qE 'health|backup-cycle|config-check' "$MAKE_LOG"; then + fail "all: NIE powinno zawierac health/backup/config" else pass "all: bez health/backup" fi diff --git a/tests/test_makefile.sh b/tests/test_makefile.sh index b55ce57..6142177 100755 --- a/tests/test_makefile.sh +++ b/tests/test_makefile.sh @@ -2108,6 +2108,143 @@ _rlc_access() { # ile_linii status upstream_time [minuta] }' } +test_configure_registry() { + yellow "=== Test: rejestr zmiennych operatora (make configure / ask-missing-config / config-check) ===" + local cfg out env sc="$REPO_DIR/scripts/configure.sh" + local fd='DJANGO_BPP_FRESHDESK_WIDGET' + + # --- sam rejestr: 7 kolumn, znane typy, domyslne przechodza wlasna walidacje, + # ${VAR} w DOMYSLNA tylko do zmiennych WYZEJ (inaczej rozwinie sie pusto) + out="$(bash -c ' + . "$1/scripts/lib-config-registry.sh" + awk -F"\t" "!/^#/ && NF && NF != 7 { print \"zla liczba kolumn: \" \$1 }" "$REJ_PLIK" + widziane=" BPP_DOMYSLNY_KATALOG_BACKUPOW " + while IFS=$'"'"'\037'"'"' read -r n g t d p u o; do + case "$p$u" in taktak|taknie|nietak|nienie) ;; *) echo "$n: PYTAJ/PUSTA to tak|nie";; esac + for ref in $(printf "%s" "$d" | grep -oE "\\\$\\{[A-Z_0-9]+\\}" | tr -d "\${}"); do + case "$widziane" in *" $ref "*) ;; *) echo "$n: DOMYSLNA siega do $ref spoza zmiennych wyzej";; esac + done + case "$d" in *"\${"*) ;; *) m="$(rej_waliduj "$t" "$u" "$d")" || echo "$n: domyslna \"$d\" niepoprawna: $m";; esac + widziane="$widziane$n " + done < <(rej_wiersze) + ' _ "$REPO_DIR" 2>&1)" + if [ -z "$out" ]; then pass "rejestr: spojny (kolumny, typy, domyslne, kolejnosc odwolan)"; else fail "rejestr: $out"; fi + + # --- make up na starej instalacji: brakuje tylko Freshdeska, Enter x3 -> decyzja zapisana + cfg="$(mktemp -d)" + printf 'DJANGO_BPP_ADMIN_EMAIL=a@b.pl\nDJANGO_BPP_SLACK_WEBHOOK=\nROLLBAR_ACCESS_TOKEN=\nDJANGO_BPP_GOOGLE_ANALYTICS_PROPERTY_ID=\nDJANGO_BPP_GOOGLE_VERIFICATION_CODE=\nDJANGO_BPP_HOST_BACKUP_DIR=/srv/b' > "$cfg/.env" + chmod 600 "$cfg/.env" + out="$(printf '\n\n\n' | BPP_CO_ASSUME_TTY=1 BPP_CONFIGS_DIR="$cfg" bash "$sc" --missing 2>&1)" \ + || fail "configure.sh --missing zakonczyl sie bledem" + env="$(cat "$cfg/.env")" + _diag_ma "rejestr: ostatnia linia bez \\n nie sklejona" '^DJANGO_BPP_HOST_BACKUP_DIR=/srv/b$' "$env" + _diag_ma "rejestr: pyta tylko o brakujace (3)" 'Nowe ustawienia BPP \(3\)' "$out" + _diag_ma "rejestr: Enter na ID zalogowanych = domyslne BPP" "^${fd}_ID=205000000433\$" "$env" + _diag_ma "rejestr: Enter na ANON = pusta (decyzja)" "^${fd}_ID_ANON=\$" "$env" + _diag_ma "rejestr: Enter na sekret = pusty (decyzja)" "^${fd}_JWT_SECRET=\$" "$env" + _diag_nie_ma "rejestr: make up NIE dopisuje zmiennych PYTAJ=nie" '^DJANGO_BPP_(SSL_MODE|BACKUP_KEEP_LAST)=' "$env" + if [ "$(stat -f '%Lp' "$cfg/.env" 2>/dev/null || stat -c '%a' "$cfg/.env")" = 600 ]; then + pass "rejestr: zapis zachowuje tryb 0600 .env (hasla)" + else + fail "rejestr: zapis zmienil tryb .env" + fi + + # --- drugi make up: decyzja zapisana -> zero pytan, zero zmian + out="$(printf 'NIE-CZYTAJ\n' | BPP_CO_ASSUME_TTY=1 BPP_CONFIGS_DIR="$cfg" bash "$sc" --missing 2>&1)" \ + || fail "configure.sh --missing (drugi raz) zakonczyl sie bledem" + if [ -z "$out" ] && [ "$(cat "$cfg/.env")" = "$env" ]; then + pass "rejestr: pusta wartosc = decyzja, make up juz nie pyta" + else + fail "rejestr: make up pyta ponownie mimo zapisanej decyzji: $out" + fi + + # --- make configure: grupa Freshdesk (6): zle ID, '-', nowe ANON, sekret z '$' odrzucony; z = zapis + # shellcheck disable=SC2016 # dosłowne '$' — to ono ma zostać odrzucone + out="$(printf '6\nabc\n-\n205000000999\nab$c\nSeKr3t/x=\nz\n' | BPP_CO_ASSUME_TTY=1 BPP_CONFIGS_DIR="$cfg" bash "$sc" 2>&1)" \ + || fail "make configure zakonczyl sie bledem" + env="$(cat "$cfg/.env")" + _diag_ma "rejestr: menu pokazuje grupy" '6\) Freshdesk' "$out" + _diag_ma "rejestr: zle ID -> pytanie ponownie" 'ID widgetu to liczba' "$out" + _diag_ma "rejestr: '\$' odrzucony (Makefile/Compose)" 'niedozwolony znak' "$out" + _diag_ma "rejestr: '-' czysci wartosc" "^${fd}_ID=\$" "$env" + _diag_ma "rejestr: nowe ANON zapisane" "^${fd}_ID_ANON=205000000999\$" "$env" + _diag_ma "rejestr: sekret zapisany" "^${fd}_JWT_SECRET=SeKr3t/x=\$" "$env" + _diag_nie_ma "rejestr: sekret nie wypisany na ekran" 'SeKr3t' "$out" + if [ "$(grep -c "^${fd}_" "$cfg/.env")" = 3 ]; then + pass "rejestr: nadpisanie bez duplikatow" + else + fail "rejestr: zdublowane zmienne ${fd}_* w .env" + fi + # q porzuca zmiany + printf '1\nq@q.pl\nq\n' | BPP_CO_ASSUME_TTY=1 BPP_CONFIGS_DIR="$cfg" bash "$sc" >/dev/null 2>&1 || true + _diag_ma "rejestr: 'q' porzuca niezapisane zmiany" '^DJANGO_BPP_ADMIN_EMAIL=a@b.pl$' "$(cat "$cfg/.env")" + + # --- config-check: poprawne -> 0; zly e-mail -> 1 z nazwa zmiennej + if BPP_CONFIGS_DIR="$cfg" bash "$sc" --check >/dev/null 2>&1; then + pass "rejestr: config-check OK na poprawnym .env" + else + fail "rejestr: config-check zglasza bledy na poprawnym .env" + fi + sed -i.bak 's/^DJANGO_BPP_ADMIN_EMAIL=.*/DJANGO_BPP_ADMIN_EMAIL=nie-email/' "$cfg/.env" + if out="$(BPP_CONFIGS_DIR="$cfg" bash "$sc" --check 2>&1)"; then + fail "rejestr: config-check przepuscil zly e-mail" + else + _diag_ma "rejestr: config-check wskazuje zla zmienna" 'DJANGO_BPP_ADMIN_EMAIL=nie-email' "$out" + fi + rm -rf "$cfg" + + # --- przerwanie w polowie (EOF/timeout): nic nie zapisane, make up idzie dalej + cfg="$(mktemp -d)"; : > "$cfg/.env" + if printf 'x@y.pl\n' | BPP_CO_ASSUME_TTY=1 BPP_CONFIGS_DIR="$cfg" bash "$sc" --missing >/dev/null 2>&1 \ + && [ ! -s "$cfg/.env" ]; then + pass "rejestr: brak odpowiedzi = nic nie zapisane, exit 0" + else + fail "rejestr: przerwany kreator zapisal czesc odpowiedzi albo wywrocil make up" + fi + if printf '' | BPP_CO_ASSUME_TTY=1 BPP_CONFIG_PROMPT_TIMEOUT=1 BPP_CONFIGS_DIR="$cfg" bash "$sc" --missing >/dev/null 2>&1; then + pass "rejestr: timeout nie blokuje make up" + else + fail "rejestr: timeout wywraca make up" + fi + + # --- bez terminala i pod autoupdate (BPP_SKIP_HEALTH_GATE): nie pyta + out="$(BPP_CONFIGS_DIR="$cfg" bash "$sc" --missing &1)" || fail "rejestr: nie-TTY wywraca make up" + _diag_ma "rejestr: nie-TTY tylko przypomina" 'make configure' "$out" + out="$(printf '\n\n\n' | BPP_SKIP_HEALTH_GATE=1 BPP_CO_ASSUME_TTY=1 BPP_CONFIGS_DIR="$cfg" bash "$sc" --missing 2>&1)" \ + || fail "rejestr: BPP_SKIP_HEALTH_GATE wywraca make up" + if [ -z "$out" ] && [ ! -s "$cfg/.env" ]; then + pass "rejestr: BPP_SKIP_HEALTH_GATE (autoupdate pod screenem) = bez pytan" + else + fail "rejestr: kreator pyta mimo BPP_SKIP_HEALTH_GATE: $out" + fi + + # --- init-configs (--init): EOF = domyslne, PYTAJ=nie dopisane, DOMYSLNA siega do odpowiedzi wyzej + printf 'x@y.pl\n' | BPP_CONFIGS_DIR="$cfg" bash "$sc" --init >/dev/null 2>&1 || fail "configure.sh --init zakonczyl sie bledem" + env="$(cat "$cfg/.env")" + _diag_ma "rejestr: --init zapisuje odpowiedz" '^DJANGO_BPP_ADMIN_EMAIL=x@y.pl$' "$env" + _diag_ma "rejestr: LETSENCRYPT_EMAIL = niezapisany jeszcze ADMIN_EMAIL" '^DJANGO_BPP_LETSENCRYPT_EMAIL=x@y.pl$' "$env" + _diag_ma "rejestr: --init dopisuje PYTAJ=nie z domyslna" '^DJANGO_BPP_SSL_MODE=manual$' "$env" + _diag_ma "rejestr: katalog backupow obok katalogu konfiguracyjnego" "^DJANGO_BPP_HOST_BACKUP_DIR=$(dirname "$cfg")/backups\$" "$env" + rm -rf "$cfg" + + # --- tabela w docs zgodna z rejestrem + if bash "$REPO_DIR/scripts/render-config-docs.sh" --check >/dev/null 2>&1; then + pass "rejestr: tabela w docs/konfiguracja/zmienne.md aktualna" + else + fail "rejestr: tabela w docs nieaktualna — make docs-config-table" + fi + + # --- podpiecie pod make up / up-quick / refresh + local t + for t in up up-quick refresh; do + if grep -qE "^${t}:.* ask-missing-config( |$)" "$REPO_DIR/mk/deployment.mk"; then + pass "rejestr: make $t pyta o brakujace zmienne" + else + fail "rejestr: make $t bez prerekwizytu ask-missing-config" + fi + done +} + test_configure_rate_limits() { yellow "=== Test: kreator limitow nginx-a (make configure-rate-limits) ===" local fx cfg out env @@ -2666,6 +2803,7 @@ test_rclone_service_declared test_backup_pg_image_retired test_diag_db_commands test_configure_rate_limits +test_configure_registry echo "" echo "========================================" From 866ac2915969c0dfa6b62242bd1613a4a39030ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Wed, 21 Oct 2026 21:29:01 +0200 Subject: [PATCH 2/2] fix(config): rejestr odporny na CRLF (Windows CI) + przenosny test trybu .env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rej_wiersze i render-config-docs.sh zdejmuja \r: checkout na Windows bez .gitattributes daje CRLF, \r wpadal do OPIS, a znaczniki w docs nie pasowaly (render --check przechodzil wtedy zawsze — zweryfikowane mutacja). - test trybu .env: GNU stat najpierw (GNU `stat -f` nie zawodzi, wiec fallback na BSD nigdy nie ruszal), porownanie przed/po zamiast 600 (na Windows chmod nie dziala). Co-Authored-By: Claude Opus 5.5 --- scripts/lib-config-registry.sh | 3 ++- scripts/render-config-docs.sh | 10 +++++++--- tests/test_makefile.sh | 9 +++++++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/scripts/lib-config-registry.sh b/scripts/lib-config-registry.sh index 59f154e..04d835c 100644 --- a/scripts/lib-config-registry.sh +++ b/scripts/lib-config-registry.sh @@ -19,7 +19,8 @@ REJ_PLIK="${REJ_PLIK:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/config-regis # (tab to "bialy" IFS), wiec pusta kolumna DOMYSLNA przesuwalaby pola. # Czytaj: while IFS=$'\037' read -r nazwa grupa typ dom pytaj pusta opis rej_wiersze() { - awk -F'\t' -v OFS='\037' '!/^#/ && NF == 7 { $1 = $1; print }' "$REJ_PLIK" + # sub(\r): checkout na Windows bez .gitattributes daje CRLF — \r wpadalby do OPIS + awk -F'\t' -v OFS='\037' '{ sub(/\r$/, "") } !/^#/ && NF == 7 { $1 = $1; print }' "$REJ_PLIK" } # Grupy w kolejnosci pierwszego wystapienia, po jednej w linii. diff --git a/scripts/render-config-docs.sh b/scripts/render-config-docs.sh index f823758..5a7e6e9 100644 --- a/scripts/render-config-docs.sh +++ b/scripts/render-config-docs.sh @@ -11,8 +11,12 @@ DOC="${BPP_CONFIG_DOC:-$REPO_DIR/docs/konfiguracja/zmienne.md}" START='' END='' +# Bez \r: checkout na Windows bez .gitattributes daje CRLF, a znaczniki +# i porownanie nizej sa liniowe. +DOC_TXT="$(tr -d '\r' < "$DOC")" + for z in "$START" "$END"; do - if [ "$(grep -cxF -- "$z" "$DOC")" != 1 ]; then + if [ "$(printf '%s\n' "$DOC_TXT" | grep -cxF -- "$z")" != 1 ]; then echo "BLAD: w $DOC musi byc dokladnie jeden znacznik: $z" >&2 exit 1 fi @@ -23,10 +27,10 @@ NOWY="$(START="$START" END="$END" TABELA="$TABELA" awk ' $0 == ENVIRON["START"] { print; print ""; print ENVIRON["TABELA"]; print ""; pomin = 1; next } $0 == ENVIRON["END"] { pomin = 0 } !pomin { print } -' "$DOC")" +' <<<"$DOC_TXT")" if [ "${1:-}" = --check ]; then - if [ "$NOWY" != "$(cat "$DOC")" ]; then + if [ "$NOWY" != "$DOC_TXT" ]; then echo "Tabela zmiennych w $DOC jest nieaktualna wzgledem scripts/config-registry.tsv — uruchom: make docs-config-table" >&2 exit 1 fi diff --git a/tests/test_makefile.sh b/tests/test_makefile.sh index 6142177..a42bf44 100755 --- a/tests/test_makefile.sh +++ b/tests/test_makefile.sh @@ -2134,6 +2134,11 @@ test_configure_registry() { cfg="$(mktemp -d)" printf 'DJANGO_BPP_ADMIN_EMAIL=a@b.pl\nDJANGO_BPP_SLACK_WEBHOOK=\nROLLBAR_ACCESS_TOKEN=\nDJANGO_BPP_GOOGLE_ANALYTICS_PROPERTY_ID=\nDJANGO_BPP_GOOGLE_VERIFICATION_CODE=\nDJANGO_BPP_HOST_BACKUP_DIR=/srv/b' > "$cfg/.env" chmod 600 "$cfg/.env" + # GNU najpierw: GNU `stat -f` NIE zawodzi (to statystyki systemu plikow), + # wiec fallback na BSD musi isc w druga strone. Porownujemy tryb przed/po + # (na Windows chmod 600 nie dziala, a i tak tryb nie moze sie zmienic). + local tryb_przed + tryb_przed="$(stat -c '%a' "$cfg/.env" 2>/dev/null || stat -f '%Lp' "$cfg/.env")" out="$(printf '\n\n\n' | BPP_CO_ASSUME_TTY=1 BPP_CONFIGS_DIR="$cfg" bash "$sc" --missing 2>&1)" \ || fail "configure.sh --missing zakonczyl sie bledem" env="$(cat "$cfg/.env")" @@ -2143,8 +2148,8 @@ test_configure_registry() { _diag_ma "rejestr: Enter na ANON = pusta (decyzja)" "^${fd}_ID_ANON=\$" "$env" _diag_ma "rejestr: Enter na sekret = pusty (decyzja)" "^${fd}_JWT_SECRET=\$" "$env" _diag_nie_ma "rejestr: make up NIE dopisuje zmiennych PYTAJ=nie" '^DJANGO_BPP_(SSL_MODE|BACKUP_KEEP_LAST)=' "$env" - if [ "$(stat -f '%Lp' "$cfg/.env" 2>/dev/null || stat -c '%a' "$cfg/.env")" = 600 ]; then - pass "rejestr: zapis zachowuje tryb 0600 .env (hasla)" + if [ "$(stat -c '%a' "$cfg/.env" 2>/dev/null || stat -f '%Lp' "$cfg/.env")" = "$tryb_przed" ]; then + pass "rejestr: zapis zachowuje tryb .env (hasla; tu ${tryb_przed})" else fail "rejestr: zapis zmienil tryb .env" fi