diff --git a/astrololo/README-astrodemo.md b/astrololo/README-astrodemo.md new file mode 100644 index 0000000..9a39f91 --- /dev/null +++ b/astrololo/README-astrodemo.md @@ -0,0 +1,197 @@ +# astrodemo — wdrożenie wersji demo (PRE-28/29) + +Osobna usługa o dwóch funkcjach: **dodanie pliku bazy** i **zapytanie o +interpretację urodzeniową**. Opis aplikacji: `services/astrodemo/README.md` +w repo aplikacji. + +--- + +## Jak to jest odizolowane + +``` +astrodemo → logic-demo → data-demo → /mnt/Tank1/astrololo-demo + └── klientA/ ← pula konta + └── klientB/ ← pula konta +``` + +**Oryginalne bazy są dla demo nieosiągalne.** Nie chodzi o uprawnienia: demo ma +własną warstwę danych, pracującą na osobnym udziale, pustym na starcie. + +Dlaczego osobna jest też **warstwa logiczna**: zna ona jeden adres warstwy danych, +więc astrodemo korzystający z produkcyjnej logiki i tak trafiłby na produkcyjne +bazy. Izolacja musi sięgnąć obu warstw naraz, inaczej nie ma jej wcale. + +**Każde konto ma własną pulę** — swój podkatalog na tym udziale. Konta nie widzą +swoich plików nawzajem ani w liście, ani w wynikach wyszukiwania. Pula bierze się +z **loginu zalogowanej osoby**, nigdy z pola formularza. + +--- + +## Krok 1 — udział `astrololo-demo` na TrueNAS + +Ta sama procedura co przy `astrololo-state`, więc jeśli tamten działa, ten też +zadziała. Po SSH na NAS: + +```bash +sudo zfs create Tank1/astrololo-demo # albo: sudo mkdir -p /mnt/Tank1/astrololo-demo +``` + +```bash +midclt call sharing.nfs.query '[["path","=","/mnt/Tank1/astrololo-demo"]]' \ + | python3 -m json.tool +``` + +**Pusta lista `[]`** — twórz: + +```bash +midclt call sharing.nfs.create '{ + "path": "/mnt/Tank1/astrololo-demo", + "comment": "astrololo — pule kont wersji demo (PRE-29)", + "hosts": ["192.168.1.73", "192.168.1.80", "192.168.1.81"], + "enabled": true, + "ro": false, + "mapall_user": "root", + "mapall_group": "root" +}' +``` + +**Coś zwróciło** — weź `id` i użyj `sharing.nfs.update ` z tą samą treścią. + +```bash +midclt call service.restart nfs +sudo exportfs -v | grep astrololo-demo +``` + +Musi tam być `rw`. Sprawdź jeszcze zapis z węzła — to jest ten test, którego +zabrakło przy poprzednim udziale: + +```bash +ssh 'sudo mkdir -p /mnt/t && sudo mount -t nfs 192.168.1.34:/mnt/Tank1/astrololo-demo /mnt/t \ + && sudo touch /mnt/t/proba && echo ZAPIS-OK || echo ZAPIS-NIE; sudo rm -f /mnt/t/proba; sudo umount /mnt/t' +``` + +--- + +## Krok 2 — konta demo + +Każde konto to osobna pula, więc **rozdajesz konta, nie jedno hasło**. + +Hasła najlepiej jako hash — wtedy nie leżą nigdzie jawnie (skrypt jest w repo +aplikacji, `services/presentation/scripts/make_user.py`): + +```bash +python scripts/make_user.py klientA # wypisze: scrypt$… +python scripts/make_user.py klientB +``` + +```bash +kubectl -n astrololo create secret generic astrololo-astrodemo \ + --from-literal=ASTRODEMO_USERS='klientA:scrypt$…,klientB:scrypt$…' \ + --from-literal=SESSION_SECRET="$(openssl rand -hex 32)" +``` + +`SESSION_SECRET` podpisuje ciasteczka sesji (LOG-34). **Pod bez niego celowo nie +wstanie.** Jest WŁASNY, nie ten z pełnej aplikacji: demo i produkcja nie mają +powodu uznawać nawzajem swoich sesji. + +> **Zdalne unieważnienie w demo działa inaczej niż w pełnej wersji.** Demo nie ma +> własnego wolumenu, więc nie ma gdzie trzymać licznika sesji. Zamiast tego: +> usunięcie konta z `ASTRODEMO_USERS` albo zmiana jego hasła **natychmiast ubija jego +> otwarte sesje** — bo odcisk poświadczenia wpisany w ciasteczko przestaje pasować. +> Rotacja `SESSION_SECRET` wylogowuje wszystkich naraz. + +### Dodanie konta później + +```bash +STARE=$(kubectl -n astrololo get secret astrololo-astrodemo -o jsonpath='{.data.ASTRODEMO_USERS}' | base64 -d) + +kubectl -n astrololo create secret generic astrololo-astrodemo \ + --from-literal=ASTRODEMO_USERS="${STARE},klientC:scrypt\$…" \ + --dry-run=client -o yaml | kubectl apply -f - + +kubectl -n astrololo rollout restart deploy/astrodemo +``` + +### Odebranie dostępu + +Usuń wpis z `ASTRODEMO_USERS` tą samą drogą. **Pula zostaje na udziale** — pliki +klienta nie znikają, tylko przestaje być komu je pokazywać. + +--- + +## Krok 3 — wdrożenie + +```bash +kubectl apply -k astrololo +kubectl -n astrololo rollout status deploy/data-demo deploy/logic-demo deploy/astrodemo +``` + +Sprawdzenie na czas jednej sesji, bez wystawiania na świat: + +```bash +kubectl -n astrololo port-forward deploy/astrodemo 8005:8005 +``` + +Wejście z zewnątrz jest już w `ingress.yaml`: host **astrodemo.czernobog.pl**, +z przekierowaniem z http na https i tym samym certyfikatem co astrololo +(`tls.yaml` obejmuje oba hosty). + +Osobny host, a nie ścieżka `/demo` pod adresem astrololo — CELOWO. Ścieżka +dzieliłaby z pełną aplikacją pochodzenie w rozumieniu przeglądarki, czyli +i ciasteczka: wejście do jednej ruszałoby sesję w drugiej. + +Pozostaje wpis DNS: **astrodemo.czernobog.pl musi wskazywać na ten sam adres co +astrololo.czernobog.pl**. Bez tego Traefik nigdy nie zobaczy żądania. +**Celowo nie zakładam tego za Ciebie**: to decyzja, pod jakim adresem świat +zobaczy demo. + +--- + +## Import puli klienta do pełnej aplikacji + +Sedno całego układu: klient, który przechodzi na pełną wersję, **nie traci +dotychczasowych wgrań**. Jego pula to jeden katalog. + +**1. Zobacz, co tam jest** (na NAS): + +```bash +LOGIN=klientA +sudo ls -la /mnt/Tank1/astrololo-demo/$LOGIN/ +``` + +**2. Sprawdź kolizje nazw** z bazami produkcyjnymi: + +```bash +comm -12 \ + <(cd /mnt/Tank1/astrololo-demo/$LOGIN && ls *.xlsx 2>/dev/null | sort) \ + <(cd /mnt/Tank1/astrololo && ls *.xlsx 2>/dev/null | sort) +``` + +Pusto = brak kolizji. Cokolwiek się wypisze, przenieś ręcznie pod inną nazwą — +**nie nadpisuj bazy produkcyjnej**. + +**3. Skopiuj** (`-n` = nie nadpisuj niczego, co już jest): + +```bash +sudo cp -n /mnt/Tank1/astrololo-demo/$LOGIN/*.xlsx /mnt/Tank1/astrololo/ +``` + +> Kopiujemy **tylko pliki `.xlsx`**. Plik `.files-state.json` zostaje — opisuje +> stan wewnątrz puli demo i w produkcji nie ma sensu; produkcja ma własny. + +**4. Włącz je w pełnej aplikacji**: zakładka **Pliki**, nowe bazy pojawią się jako +„gotowa, odstawiona". Klikasz przełącznik przy tych, które mają wejść do użytku. + +To, że nie włączają się same, jest zamierzone: przeniesienie plików jest czynnością +techniczną, a decyzja, które bazy biorą udział w interpretacji, należy do Ciebie. + +--- + +## Odcięcie całego demo + +```bash +kubectl -n astrololo scale deploy/astrodemo --replicas=0 +``` + +Pule i dane zostają. Skasowanie sekretu `astrololo-astrodemo` zatrzyma pod (nie wstanie +bez niego) i to też jest zachowanie zamierzone. diff --git a/astrololo/README.md b/astrololo/README.md index 27460f1..3d3818c 100644 --- a/astrololo/README.md +++ b/astrololo/README.md @@ -29,6 +29,20 @@ Runbook: **[README-pliki.md](README-pliki.md)**. Ekran „Pliki" wymaga zapisu d udziału z bazami, więc znosi gwarancję z DAN-25, że baz nie da się zmienić przez NFS. Co zostaje z zabezpieczeń i jak to wdrożyć — w runbooku. +## astrodemo — wersja demonstracyjna (PRE-28/29) + +Osobna usługa o dwóch funkcjach: dodanie pliku i pytanie o interpretację +urodzeniową. Runbook: **[README-astrodemo.md](README-astrodemo.md)**. + +Ma **własną warstwę danych i logiki** (`astrodemo-stack.yaml`) oraz własny udział +NFS, a każde konto dostaje w nim odrębną, niewidoczną dla pozostałych pulę plików +(PRE-29). Nie sięga do produkcyjnych baz. + +Konta są osobne (sekret `astrololo-astrodemo`), więc usługę odcina się jedną +zmianą, bez ruszania kont głównej aplikacji. Wchodzi się przez własny host +`astrodemo.czernobog.pl` — nie przez ścieżkę pod adresem astrololo, bo wspólne +pochodzenie oznaczałoby wspólne ciasteczka i wzajemne wylogowywanie. + ## ⚠️ Wymóg węzłów: klient NFS Wszystkie warstwy montują udziały z NAS-a, więc **każdy węzeł, na którym może diff --git a/astrololo/astrodemo-stack.yaml b/astrololo/astrodemo-stack.yaml new file mode 100644 index 0000000..867751e --- /dev/null +++ b/astrololo/astrodemo-stack.yaml @@ -0,0 +1,122 @@ +# Warstwa danych i logiki WYŁĄCZNIE dla wersji demo (PRE-29). +# +# DLACZEGO OSOBNY KOMPLET, A NIE WSPÓŁDZIELONY. Warstwa logiczna zna JEDEN adres +# warstwy danych, więc astrodemo korzystający z produkcyjnej logiki i tak trafiłby +# na produkcyjne bazy. Izolacja demo musi więc sięgnąć obu warstw naraz, inaczej +# nie ma jej wcale. +# +# Udział `astrololo-astrodemo` jest PUSTY na starcie i nigdy nie zawiera oryginalnych +# baz. Każde konto demo dostaje w nim własny podkatalog (pulę) — patrz PRE-29. +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: data-demo + namespace: astrololo +spec: + replicas: 1 + selector: + matchLabels: { app: data-demo } + template: + metadata: + labels: { app: data-demo } + spec: + containers: + - name: data + # TEN SAM obraz co produkcja — różni się wyłącznie udziałem, na którym + # pracuje. Osobny obraz oznaczałby drugi kod do utrzymania i pewność, + # że kiedyś się rozjadą. + image: gitea.czernobog.pl/gitea/astrololo-data:latest + ports: [{ containerPort: 8002 }] + env: + - name: DATA_PROVIDER + value: "excel" + - name: EXCEL_DIR + value: "/app/data_files" + - name: CACHE_DIR + value: "/app/.cache" + - name: INTERNAL_TOKEN + valueFrom: + secretKeyRef: { name: astrololo-auth, key: INTERNAL_TOKEN } + - name: LINK_KEY_LOGIC_DATA + valueFrom: + secretKeyRef: { name: astrololo-link, key: LINK_KEY_LOGIC_DATA } + - name: LINK_ENCRYPTION_REQUIRED + value: "true" + volumeMounts: + - name: cache + mountPath: /app/.cache + - name: demo + mountPath: /app/data_files + resources: + requests: { cpu: "100m", memory: "256Mi" } + limits: { cpu: "500m", memory: "512Mi" } + volumes: + - name: cache + emptyDir: {} + - name: demo + nfs: + server: 192.168.1.34 + # OSOBNY udział, pusty na starcie. NIE /mnt/Tank1/astrololo — + # to jest cała istota izolacji demo. + path: /mnt/Tank1/astrololo-demo +--- +apiVersion: v1 +kind: Service +metadata: + name: data-demo + namespace: astrololo +spec: + type: ClusterIP + selector: { app: data-demo } + ports: [{ port: 8002, targetPort: 8002 }] +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: logic-demo + namespace: astrololo +spec: + replicas: 1 + selector: + matchLabels: { app: logic-demo } + template: + metadata: + labels: { app: logic-demo } + spec: + containers: + - name: logic + image: gitea.czernobog.pl/gitea/astrololo-logic:latest + ports: [{ containerPort: 8001 }] + env: + # JEDYNA różnica wobec produkcyjnej logiki — i zarazem cała izolacja. + - name: DATA_URL + value: "http://data-demo:8002" + - name: EPHEMERIS_ENGINE + value: "own" + - name: INTERNAL_TOKEN + valueFrom: + secretKeyRef: { name: astrololo-auth, key: INTERNAL_TOKEN } + - name: LINK_KEY_PRESENTATION_LOGIC + valueFrom: + secretKeyRef: { name: astrololo-link, key: LINK_KEY_PRESENTATION_LOGIC } + - name: LINK_KEY_LOGIC_DATA + valueFrom: + secretKeyRef: { name: astrololo-link, key: LINK_KEY_LOGIC_DATA } + - name: LINK_ENCRYPTION_REQUIRED + value: "true" + # Bez kluczy do modeli językowych. Astroklient nie umie o nie prosić, + # więc ich tu nie ma — czego nie ma w podzie, tego nie wyniesie. + resources: + requests: { cpu: "100m", memory: "128Mi" } + limits: { cpu: "500m", memory: "512Mi" } +--- +apiVersion: v1 +kind: Service +metadata: + name: logic-demo + namespace: astrololo +spec: + type: ClusterIP + selector: { app: logic-demo } + ports: [{ port: 8001, targetPort: 8001 }] diff --git a/astrololo/astrodemo.yaml b/astrololo/astrodemo.yaml new file mode 100644 index 0000000..099566f --- /dev/null +++ b/astrololo/astrodemo.yaml @@ -0,0 +1,86 @@ +# astrodemo — wersja demonstracyjna (PRE-28). +# +# Osobna usługa o DWÓCH funkcjach: dodanie pliku bazy i zapytanie o interpretację +# urodzeniową. Pozostałe funkcje nie są tu ukryte — nie ma ich w obrazie. +# +# IZOLACJA (PRE-29): astrodemo ma WŁASNĄ warstwę logiczną i WŁASNĄ warstwę +# danych (astrodemo-stack.yaml), pracujące na osobnym, pustym na starcie +# udziale. Oryginalne bazy interpretacyjne są dla demo NIEOSIĄGALNE. +# +# Wewnątrz demo każde konto ma własną pulę plików — swój podkatalog na tym +# udziale — niewidoczną dla pozostałych kont. Dzięki temu demo można rozdawać +# szeroko, a pliki jednego klienta da się później przenieść do pełnej aplikacji +# jako jeden katalog (patrz README-astrodemo.md). +apiVersion: apps/v1 +kind: Deployment +metadata: + name: astrodemo + namespace: astrololo +spec: + replicas: 1 + selector: + matchLabels: { app: astrodemo } + template: + metadata: + labels: { app: astrodemo } + spec: + containers: + - name: astrodemo + image: gitea.czernobog.pl/gitea/astrololo-astrodemo:latest + ports: [{ containerPort: 8005 }] + env: + # WŁASNA warstwa logiczna, wskazująca na WŁASNĄ warstwę danych. + # Wpisanie tu produkcyjnego `logic` cofnęłoby całą izolację demo + # jednym słowem — dlatego stoi to tuż obok siebie w jednym pliku. + - name: LOGIC_URL + value: "http://logic-demo:8001" + # Konto demo — WŁASNE, nie astrololo-auth. Sekret tworzony POZA repo + # (jak pozostałe), patrz README-astrodemo.md. + # KONTA DEMO — lista `login:sekret` po przecinku. Każde konto ma + # WŁASNĄ, niewidoczną dla pozostałych pulę baz (PRE-29), więc jedno + # wspólne konto oznaczałoby klientów oglądających swoje wgrania + # nawzajem. Sekrety najlepiej jako hash scrypt. + - name: ASTRODEMO_USERS + valueFrom: + secretKeyRef: { name: astrololo-astrodemo, key: ASTRODEMO_USERS } + # Token międzywarstwowy i klucz łącza — TE SAME co prezentacja: + # demo nie jest furtką omijającą ochronę warstwy logicznej. + - name: INTERNAL_TOKEN + valueFrom: + secretKeyRef: { name: astrololo-auth, key: INTERNAL_TOKEN } + - name: LINK_KEY_PRESENTATION_LOGIC + valueFrom: + secretKeyRef: { name: astrololo-link, key: LINK_KEY_PRESENTATION_LOGIC } + - name: LINK_ENCRYPTION_REQUIRED + value: "true" + # Klucz podpisu ciasteczek sesji (LOG-34). WYMAGANY — pod bez niego + # celowo nie wstaje. WŁASNY, nie ten z pełnej aplikacji: demo i produkcja + # nie mają powodu uznawać nawzajem swoich sesji, a wspólny klucz + # znaczyłby, że sesja z demo bywa ważna tam, gdzie nie powinna. + - name: SESSION_SECRET + valueFrom: + secretKeyRef: { name: astrololo-astrodemo, key: SESSION_SECRET } + # Za Ingressem bezpośrednim rozmówcą jest pod Traefika. Bez tego limit + # liczyłby WSZYSTKICH do jednego wiadra i pierwsza osoba, która go + # wyklika, odcięłaby pozostałe. + - name: TRUST_PROXY + value: "true" + # Niższy niż w pełnej aplikacji: demo bywa udostępniane szerzej, + # a każde zapytanie sięga do treści baz. + - name: RATE_LIMIT_PER_MIN + value: "60" + resources: + requests: { cpu: "50m", memory: "96Mi" } + limits: { cpu: "300m", memory: "256Mi" } +--- +apiVersion: v1 +kind: Service +metadata: + name: astrodemo + namespace: astrololo +spec: + # ClusterIP — wejście z zewnątrz wyłącznie przez Ingress po https, tak samo + # jak prezentacja. Druga, nieszyfrowana droga byłaby obejściem PRE-16. + type: ClusterIP + selector: { app: astrodemo } + ports: [{ port: 8005, targetPort: 8005 }] diff --git a/astrololo/image-updater.yaml b/astrololo/image-updater.yaml index 5eb4ce5..cb82916 100644 --- a/astrololo/image-updater.yaml +++ b/astrololo/image-updater.yaml @@ -34,6 +34,10 @@ spec: imageName: gitea.czernobog.pl/gitea/astrololo-presentation - alias: render imageName: gitea.czernobog.pl/gitea/astrololo-render + # Wersja demo (PRE-28). Obraz, którego nie ma na tej liście, NIGDY się nie + # podbije, choćby CI go budowało — tak przez chwilę wisiał render na :latest. + - alias: astrodemo + imageName: gitea.czernobog.pl/gitea/astrololo-astrodemo commonUpdateSettings: updateStrategy: newest-build writeBackConfig: diff --git a/astrololo/ingress.yaml b/astrololo/ingress.yaml index 01d4f8a..c736992 100644 --- a/astrololo/ingress.yaml +++ b/astrololo/ingress.yaml @@ -73,3 +73,46 @@ spec: pathType: Prefix backend: service: { name: presentation, port: { number: 8000 } } +--- +# ── astrodemo ────────────────────────────────────────────────────────────── +# Osobny host, nie ścieżka pod astrololo.czernobog.pl. Ścieżka („/demo") dzieliłaby +# z pełną aplikacją pochodzenie w rozumieniu przeglądarki, czyli i ciasteczka: +# wejście do jednej ruszałoby sesję w drugiej. Osobny host trzyma je rozdzielnie. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: astrodemo + namespace: astrololo + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.middlewares: astrololo-security-headers@kubernetescrd +spec: + tls: + - hosts: [astrodemo.czernobog.pl] + secretName: astrololo-tls # ten sam certyfikat obejmuje oba hosty (tls.yaml) + rules: + - host: astrodemo.czernobog.pl + http: + paths: + - path: / + pathType: Prefix + backend: + service: { name: astrodemo, port: { number: 8005 } } +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: astrodemo-redirect + namespace: astrololo + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: web + traefik.ingress.kubernetes.io/router.middlewares: astrololo-redirect-https@kubernetescrd +spec: + rules: + - host: astrodemo.czernobog.pl + http: + paths: + - path: / + pathType: Prefix + backend: + service: { name: astrodemo, port: { number: 8005 } } diff --git a/astrololo/kustomization.yaml b/astrololo/kustomization.yaml index 000e978..9ab84e0 100644 --- a/astrololo/kustomization.yaml +++ b/astrololo/kustomization.yaml @@ -5,6 +5,8 @@ resources: - postgres.yaml # lustro baz Excela w SQL (DAN-28) — sekret astrololo-postgres POZA repo - data.yaml - logic.yaml + - astrodemo-stack.yaml # OSOBNA warstwa danych i logiki dla demo (PRE-29) + - astrodemo.yaml # wersja demo (PRE-28) — sekret astrololo-astrodemo POZA repo - presentation.yaml - render.yaml # składanie raportu PDF (PRE-24), osobny obraz z TeX Live - tls.yaml # certyfikat z własnego CA (wymaga cert-managera) @@ -18,3 +20,5 @@ images: newTag: latest - name: gitea.czernobog.pl/gitea/astrololo-presentation newTag: 320a0ab2 + - name: gitea.czernobog.pl/gitea/astrololo-astrodemo + newTag: latest diff --git a/astrololo/tls.yaml b/astrololo/tls.yaml index 771ae16..6aba13f 100644 --- a/astrololo/tls.yaml +++ b/astrololo/tls.yaml @@ -59,6 +59,9 @@ spec: commonName: astrololo.czernobog.pl dnsNames: - astrololo.czernobog.pl + # Jeden certyfikat na oba hosty. Osobny wymagałby osobnego sekretu i osobnego + # odnawiania, a to ten sam klaster i ten sam wystawca. + - astrodemo.czernobog.pl duration: 2160h # 90 dni renewBefore: 720h # 30 dni zapasu privateKey: { algorithm: ECDSA, size: 256, rotationPolicy: Always }