224 lines
12 KiB
Markdown
224 lines
12 KiB
Markdown
|
|
---
|
|||
|
|
okf: "0.1"
|
|||
|
|
type: decision
|
|||
|
|
visibility: private
|
|||
|
|
status: active
|
|||
|
|
updated: 2026-08-03
|
|||
|
|
links:
|
|||
|
|
- ../phases/backlog.md
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Zamknięte
|
|||
|
|
|
|||
|
|
### Publiczny bind `operator_ui.py:18180` na VPS bez autoryzacji — NAPRAWIONE (2026-07-22, commit `9a5c160`)
|
|||
|
|
|
|||
|
|
**Data**: 2026-07-22
|
|||
|
|
**Źródło**: sesja 2026-07-22/23 (`docs/sessions/2026-07-23-control-plane-remediation-e2e.md`)
|
|||
|
|
**Było**: `operator_ui.py` (port 18180) nasłuchiwał na `0.0.0.0` na publicznym VPS
|
|||
|
|
(`135.181.153.108`) bez żadnej autoryzacji. `curl` z zewnątrz do `/actions` zwracał
|
|||
|
|
HTTP 200. `do_POST /action/mutate` przenosi akcje między stanami WŁĄCZNIE z
|
|||
|
|
`"approved"` — dowolna osoba z internetu mogła zatwierdzić akcję remediacyjną.
|
|||
|
|
Jedyne co chroniło do tej pory: executor nie umiał jeszcze wykonać akcji (brak SSH,
|
|||
|
|
patrz wpis niżej) — przypadek, nie zabezpieczenie.
|
|||
|
|
**Naprawione**: dual-bind wg wzorca fleet-prometheus — `127.0.0.1:18180:8080` +
|
|||
|
|
`${TAILSCALE_BIND_IP}:18180:8080`, nowy `services/control-plane/env.example`.
|
|||
|
|
Zweryfikowane po deployu: publiczny IP → HTTP 000, Tailscale → HTTP 200. Konsumenty
|
|||
|
|
(node-agent VPS, materializer PIHA) nietknięte.
|
|||
|
|
**Footgun zapamiętany**: brak `.env` → `docker compose` tylko OSTRZEGA i po cichu
|
|||
|
|
wraca do bindu `0.0.0.0`, nie failuje — patrz wpis „deploy-local.sh: brak twardego
|
|||
|
|
checka .env" w Aktywnych.
|
|||
|
|
**Pozostaje osobno**: `operator_ui.py` nadal bez żadnej autoryzacji (bind zamknięty
|
|||
|
|
chroni przed internetem, nie przed kimkolwiek w mesh Tailscale) — patrz Aktywne.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Remediacja floty bez SSH: executor→node-agent pull przez rsync — ZROBIONE (2026-07-22, commit `2dac154`), E2E potwierdzone 2026-07-23
|
|||
|
|
|
|||
|
|
**Data**: 2026-07-22 (implementacja), 2026-07-23 (pierwszy udany cykl E2E)
|
|||
|
|
**Źródło**: sesja 2026-07-22/23 (`docs/sessions/2026-07-23-control-plane-remediation-e2e.md`)
|
|||
|
|
**Było**: łańcuch supervisor→pending→approved→running działał, ale executor próbował
|
|||
|
|
`ssh oskar@{node} docker restart {container}` — kontener control-plane nie ma klienta
|
|||
|
|
ssh, klucza, ani rozwiązywalnych nazw węzłów. Wykonanie zawsze failowało.
|
|||
|
|
**Decyzja architektoniczna**: bez SSH w executorze (kontener na publicznym VPS z
|
|||
|
|
powłoką na całą flotę = zły blast radius). Kierunek PULL: executor zleca (zapis
|
|||
|
|
`actions/dispatch/<node>/<action_id>.json`), node-agent na docelowym węźle wykonuje
|
|||
|
|
lokalnie przez własny `docker.sock`, wynik wraca eventem `action_result` przez
|
|||
|
|
istniejący rsync-push. VPS nigdy nie inicjuje połączenia do węzła.
|
|||
|
|
**Zabezpieczenia**: walidacja `node == self.node_name`; whitelist typów akcji =
|
|||
|
|
`{container_restart}`; guard przed restartem samego node-agenta; brak wykonywania
|
|||
|
|
dowolnych poleceń z payloadu; idempotencja (ponowne zlecenie = no-op). 183 testy.
|
|||
|
|
**Potwierdzone w boju (2026-07-23)**: cykl `test-e2e-b` — Executing → Dispatched →
|
|||
|
|
Completed w 31 sekund, `node_exporter` na PIHA realnie zrestartowany (uptime 30h→
|
|||
|
|
minuty), zero połączeń SSH. Pierwszy w historii systemu pełny cykl remediacji.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Uprawnienia `actions/` na PIHA (uid/gid oskar 1004 vs kontener 1000) — NAPRAWIONE ręcznie (2026-07-23, poza repo)
|
|||
|
|
|
|||
|
|
**Data**: 2026-07-23
|
|||
|
|
**Źródło**: sesja 2026-07-22/23 (`docs/sessions/2026-07-23-control-plane-remediation-e2e.md`)
|
|||
|
|
**Było**: pierwszy test E2E remediacji bez SSH padał — agent logował `[Errno 13]
|
|||
|
|
Permission denied: /opt/homelab/actions/dispatch` co cykl. `/opt/homelab/actions`
|
|||
|
|
było `oskar:oskar drwxr-xr-x` (utworzone w maju), a działający wzorzec to
|
|||
|
|
`/opt/homelab/events` = `oskar:pi drwxrwsr-x` (grupa `pi`, zapis grupowy, setgid).
|
|||
|
|
Ten sam motyw uid/gid (host oskar 1004 vs kontener 1000) uderzył już czwarty raz —
|
|||
|
|
patrz sekcja „Tech-debt: globalny porządek uid/gid/uprawnień we flocie".
|
|||
|
|
**Naprawione (ręcznie, tylko na PIHA)**: `chgrp -R pi` + `chmod -R g+w` + `chmod g+s`
|
|||
|
|
na `/opt/homelab/actions`. Executor zachował się poprawnie podczas awarii: po 300s
|
|||
|
|
timeoutu przeniósł akcję do `failed` z czytelnym powodem, nic nie zawisło.
|
|||
|
|
**Pozostaje osobno**: fix zastosowany TYLKO na PIHA — SOLARIA i lustro
|
|||
|
|
niezweryfikowane; docelowy fix systemowy to node-agent tworzący
|
|||
|
|
`dispatch/<node>/` z właściwymi prawami przy starcie — patrz Aktywne.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Supervisor: pętla zamrożona ~24h, healthy ale nie tika — NAPRAWIONE (2026-07-16, commit `409b583`)
|
|||
|
|
|
|||
|
|
**Data**: 2026-07-16
|
|||
|
|
**Źródło**: sesja 2026-07-16 (`docs/sessions/2026-07-16.md`) — wątek control-plane
|
|||
|
|
**Było**: kontener supervisora `healthy`, ale pętla `reconcile()` nie tikała od ~24h,
|
|||
|
|
zero logów. Root cause zweryfikowany na `/proc` (`State:S hrtimer_nanosleep`, NIE
|
|||
|
|
deadlock): `glob` po `EVENTS_DIR` co cykl przy 358k plikach → cykl przekracza brak
|
|||
|
|
timeoutu → nigdy się nie kończy. Logi na DEBUG maskowały objaw.
|
|||
|
|
**Naprawione**: każdy cykl w `ThreadPoolExecutor` z `future.result(timeout=90s,
|
|||
|
|
env SUPERVISOR_RECONCILE_TIMEOUT)`; try/except owija cykl (wyjątek nie zabija pętli);
|
|||
|
|
tick-log co 10 cykli (`SUPERVISOR_TICK_LOG_EVERY`) na INFO; healthcheck sprawdza
|
|||
|
|
świeżość heartbeat, nie tylko czy proces żyje. Zweryfikowane w boju: pętla tika
|
|||
|
|
(cycle #340→#480), cykl #1 timeoutował ale pętla kontynuowała = odporność działa.
|
|||
|
|
**Lekcja**: „healthy kontener ≠ tikająca pętla" — healthcheck musi sprawdzać
|
|||
|
|
świeżość ostatniego cyklu, nie samo czy proces odpowiada.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Event flood 358k plików + retencja martwa od fixu checkpointu — NAPRAWIONE (2026-07-16, commit `dff76ec`)
|
|||
|
|
|
|||
|
|
**Data**: 2026-07-16
|
|||
|
|
**Źródło**: sesja 2026-07-16 (`docs/sessions/2026-07-16.md`) — wątek control-plane
|
|||
|
|
**Było**: `EVENTS_DIR` = 358k plików, 91% to `service_healthy` emitowany co cykl per
|
|||
|
|
serwis (stan-jako-zdarzenie, antywzorzec). Retencja `_cleanup_control_plane_fs()`
|
|||
|
|
była martwa od fixu checkpointu `d5139c9` (2026-07-15, patrz „Bug: checkpoint
|
|||
|
|
observera po ścieżce leksykalnej" niżej) — porównanie `str(ścieżka) <= checkpoint_int`
|
|||
|
|
rzucało `TypeError` cicho łapany przez szeroki `except` → backlog rósł bez ograniczenia.
|
|||
|
|
Naprawiając checkpoint wczoraj, złamaliśmy retencję, która na nim polegała.
|
|||
|
|
**Naprawione**: (a) node-agent emituje `service_healthy` tylko na transition
|
|||
|
|
unhealthy→healthy (funkcja dla `observer.process_event` zachowana); (b) retencja
|
|||
|
|
naprawiona epoch-do-epoch; (c) `scripts/maintenance/cleanup_event_backlog.py`
|
|||
|
|
(dry-run + `--apply`). 150 testów pass. Cleanup wykonany na PIHA+VPS: 272 232 pliki
|
|||
|
|
usunięte, backlog 358k→12,7k. **Wynik**: reconcile supervisora przestał timeoutować.
|
|||
|
|
**Lekcja**: migracja typu pola (ścieżka→epoch int) musi audytować WSZYSTKICH
|
|||
|
|
konsumentów tego pola — szeroki `except` maskował dokładnie tę klasę regresji.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Ghost kontenery w panelu (Problem B) — ZNIKNĘŁY (potwierdzone reconem 2026-07-02)
|
|||
|
|
|
|||
|
|
> ⚠️ ZDEZAKTUALIZOWANE 2026-07-06: ghosty znów widoczne na VPS — patrz wpis
|
|||
|
|
> „Ghosty B WRÓCIŁY" w Aktywnych (`docs/sessions/2026-07-06.md`).
|
|||
|
|
|
|||
|
|
**Data**: 2026-06-24 (wykryte), 2026-07-02 (zamknięte)
|
|||
|
|
**Źródło**: sesje 2026-06-24/25/26; recon `docs/infra/inventory-verify-2026-07-02.md`
|
|||
|
|
**Było**: martwe kontenery ze starych project-name'ów
|
|||
|
|
(`8547b46c0317_control-plane-supervisor` itp.) raportowane przez observera jako
|
|||
|
|
`error` → `System Status ERROR` w panelu mimo zdrowego mózgu.
|
|||
|
|
**Zamknięte**: recon 2026-07-02 — 24 kontenery na VPS, ZERO hash-prefixed.
|
|||
|
|
Prawdopodobnie recreate'y z kolejnych deployów je zmiotły. Zero akcji ręcznej.
|
|||
|
|
Rozważenie czyszczenia obcych project-name przy deployu — już nieaktualne
|
|||
|
|
(fix A `3b71707` blokuje źródło divergence).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### brain-watchdog: poll Prometheus — POTWIERDZONY (recon 2026-07-02)
|
|||
|
|
|
|||
|
|
**Data**: 2026-06-30 (pending), 2026-07-02 (zamknięte)
|
|||
|
|
**Źródło**: recon `docs/infra/inventory-verify-2026-07-02.md`
|
|||
|
|
**Było**: log startowy nie wypisuje `PROMETHEUS_URL` → brak pewności, że polling
|
|||
|
|
aktywny; diagnoza opierała się na `.env` i braku błędów.
|
|||
|
|
**Zamknięte**: recon potwierdził — obraz zbudowany po `62d6fc0`, `PROMETHEUS_URL`
|
|||
|
|
w `.env` I w env kontenera, zero `poll failed` w logach. Poll aktywny.
|
|||
|
|
Jednolinijkowy log startowy (`polling enabled/disabled`) pozostaje opcjonalną
|
|||
|
|
kosmetyką. ✅ Pełne end-to-end (firing → Telegram) POTWIERDZONE 2026-07-06
|
|||
|
|
testem `AlertTestEtap0` (Etap 0 cutoveru, `docs/sessions/2026-07-06.md`) —
|
|||
|
|
bez czekania na realną awarię.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Miny #1/#2/#3 z weryfikacji inwentaryzacji — ROZBROJONE (2026-07-02)
|
|||
|
|
|
|||
|
|
**Źródło**: `docs/infra/inventory-verify-2026-07-02.md`, sesja
|
|||
|
|
`docs/sessions/2026-07-02.md` (tam szczegóły i lekcje).
|
|||
|
|
- **#1 PIHA checkout**: gałąź wciąż `task/kb-gmail-import` po resecie z 2026-06-30
|
|||
|
|
(reset --hard przesuwa gałąź, nie przełącza) → `checkout master && pull`,
|
|||
|
|
30 commitów nadrobione.
|
|||
|
|
- **#2 control-plane na SATURN**: supervisor ślepy (brak mountu repo) →
|
|||
|
|
`docker compose down`, wolumeny zachowane. Jedyny mózg = VPS.
|
|||
|
|
- **#3 owner_node**: forgejo→piha, mosquitto→vps (commit `886bc85`).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 🔴 KRYTYCZNY — `deploy.sh vps` niszczył control-plane — NAPRAWIONE (commit `3b71707`)
|
|||
|
|
|
|||
|
|
**Data**: 2026-06-24 (wykryte), 2026-06-25 (incydent w produkcji), 2026-06-26 (naprawione)
|
|||
|
|
**Źródło**: sesja 2026-06-24 + 2026-06-25 + 2026-06-26 (`docs/sessions/2026-06-26.md`)
|
|||
|
|
**Było (Problem A)**: `control-plane` w `hosts/vps/services.yaml` jako zwykły serwis
|
|||
|
|
pętli `deploy-node.sh`. Pętla używała innego `COMPOSE_PROJECT_NAME` niż `deploy-local.sh`
|
|||
|
|
(cwd=`services/control-plane`). Niezgodność → Recreate → `No such container:
|
|||
|
|
<hash>_control-plane-observer` → `set -e` przerywa pętlę → observer/supervisor/executor/ui
|
|||
|
|
znikają. Każdy `deploy.sh vps` rozkładał mózg (potwierdzone w produkcji 2026-06-25).
|
|||
|
|
**Naprawione**: guard w `deploy-node.sh` pomijający serwisy z własnym
|
|||
|
|
`services/<svc>/deploy-local.sh`. `control-plane` ZOSTAJE w `services.yaml` (gate
|
|||
|
|
pytest+build nadal go testuje), pomijana jest tylko destrukcyjna pętla deployu.
|
|||
|
|
**Potwierdzone w boju**: `deploy.sh vps` wypisał `Skipping control-plane: ma własną
|
|||
|
|
ścieżkę deployu`, mózg `Up 25h healthy`, nietknięty.
|
|||
|
|
**Pozostało osobno**: ghosty z poprzednich rozjazdów (Problem B — patrz Aktywne).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Flaky testy control-plane — state-leak w pytest — NAPRAWIONE (commit `992ff7c`)
|
|||
|
|
|
|||
|
|
**Data**: 2026-06-24 (zgłoszone), 2026-06-25 (naprawione)
|
|||
|
|
**Źródło**: sesja 2026-06-24 + sesja 2026-06-25 (`docs/sessions/2026-06-25.md`)
|
|||
|
|
**Było**: `test_incident_lifecycle.py` flaky przez state-leak — `OBSERVER_STATE_FILE`
|
|||
|
|
wyprowadzany przy imporcie, helpery patchowały `STATE_DIR` ale nie `OBSERVER_STATE_FILE` →
|
|||
|
|
checkpointy pisane na realny dysk `/opt/homelab/state/` z ścieżkami otagowanymi numerem
|
|||
|
|
przebiegu pytest. Gate deploy.sh czerwony przy zdrowym kodzie.
|
|||
|
|
**Naprawione**: autouse monkeypatch fixture redirectujący WSZYSTKIE ścieżki stanu w tym
|
|||
|
|
`OBSERVER_STATE_FILE`; usunięto buggy `_make_observer`; posprzątano zatruty realny checkpoint.
|
|||
|
|
Weryfikacja: 6/6 przebiegów → 28 passed. Gate rzetelny.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### deploy-node.sh — brak `--env-file` (bind 0.0.0.0) — NAPRAWIONE (commit `686aca7`)
|
|||
|
|
|
|||
|
|
**Data**: 2026-06-25
|
|||
|
|
**Źródło**: sesja 2026-06-25 (`docs/sessions/2026-06-25.md`)
|
|||
|
|
**Było**: `deploy-node.sh` nie przekazywał `--env-file` do `compose up` → zmienne `.env`
|
|||
|
|
(jak `TAILSCALE_BIND_IP`) interpolowały się do pustego stringa → bind `0.0.0.0` zamiast
|
|||
|
|
Tailscale IP. Potencjalna dziura na publicznym VPS.
|
|||
|
|
**Naprawione**: guard `if [ -f "services/<svc>/.env" ]` + `--env-file` per-serwis przed
|
|||
|
|
`docker compose up`. Worktree `task/deploy-envfile-fix`, merge ff-only.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Swap 2–4 GB na VPS — ZROBIONE
|
|||
|
|
|
|||
|
|
**Data**: 2026-06-22 (zgłoszone PENDING w sesji 2026-06-09 flota-recovery)
|
|||
|
|
**Źródło**: sesja 2026-06-22 (`docs/sessions/2026-06-22.md`)
|
|||
|
|
**Było**: VPS (3.7 GB RAM) z `swap=0` → OOM 2026-06-01.
|
|||
|
|
**Zrobione**: `/swapfile` 4 GB aktywny i trwały (wpis w `/etc/fstab`); `vm.swappiness=10`
|
|||
|
|
na żywo i w `/etc/sysctl.conf`. Weryfikacja: `free -h` → `Swap: 4.0Gi (0B used)`,
|
|||
|
|
`/proc/swaps` zawiera `/swapfile`.
|
|||
|
|
**Uwaga**: host-level one-off (nie czysto GitOps), udokumentowany jako celowy host-state
|
|||
|
|
w `hosts/vps/host.yaml`. Brak commitu kodu — zmiana host-side gotowcem.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Observer staleness — martwy node pokazywany NOMINAL
|
|||
|
|
|
|||
|
|
**Data**: 2026-06-08 (złapane), status: OTWARTY w sensie implementacji
|
|||
|
|
**Problem**: observer/supervisor trzyma ostatni znany stan; brak heartbeat TTL.
|
|||
|
|
Chelsty-infra milczy, ale status NOMINAL podważa zaufanie do panelu.
|
|||
|
|
**Fix**: heartbeat TTL → po przekroczeniu oznacz status `stale` lub `down`.
|
|||
|
|
**Powiązane**: brain-watchdog ślepy na per-node freshness.
|
|||
|
|
*(Otwarty jako TODO implementacyjny — przeniesiony z sesji 2026-06-08)*
|
|||
|
|
|