Netpulse_SasS/docker-compose.yml
byrsapty ed8fc831bf Дві сесії роботи: 0058–0068, розгортання однією командою, тести
Один коміт, а не десяток тематичних, свідомо: теми переплетені в
спільних файлах (store.go, docker-compose.yml, deploy/README.md), і
розділити їх можна було б лише індексуванням шматків. Коміти, які не
збираються, гірші за один великий — тим паче що це рівно той стан, який
перевірявся разом.

ЩО ПРАЦЮЄ НА СТЕНДІ Й ПЕРЕВІРЕНО ТАМ

  0058  подієві алерти: syslog, ncm, compliance спрацьовують у мить
        події; правило з нереалізованим джерелом більше не зберігається
        мовчки
  0059  snmp.walk і прототипи шаблонів — таблиці з динамічним індексом
        описуються шаблоном, а не Go
  0060  відкат конфігу: план як різниця, маскування паролів із підписом
        плану, обов'язковий контрольний збір, verifying при обриві
  0061  кнопки Telegram: довге опитування, авторизація не з callback_data
  0062  аудит і архів хостів; тест на AST, що падає на ключі без назви
  0063  RLS: три ролі, окремий пул для фонових тактів
  0064  строки зберігання даних і сторінка сховища
  0065  приймач SNMP-трапів; перевірено справжніми пакетами по дроту,
        переклад v1→v2 за RFC 3584 дає правильний OID
  0066  ескалації сповіщень
  0067  алерт про вичерпання диска
  0068  поля заливки конфігу переїхали в каталог профілів

Плюс: 137 тестів вебу з нуля (їх не було взагалі), одинадцять справжніх
вад, знайдених ними й виправлених, і виправлення двох інтеграційних
тестів grpcapi, які мовчки пропускались півтора року.

ЩО ЩЕ НЕ ЗАПУСКАЛОСЬ

  netpulse            установник: одна команда замість 18 змінних і
                      593 рядків інструкції
  RLS з першого запуску  нова інсталяція під політиками одразу;
                      RLS-EXISTING-INSTALL.md лишається тільки для
                      старих інсталяцій
  .forgejo + CI       раннер не зареєстрований

Ці три перевірені компіляцією й міркуванням, але не виконанням.

ГОЛОВНИЙ ВИСНОВОК ДВОХ СЕСІЙ

Зелена перевірка доводить рівно те, що вона перевіряє. Тест ізоляції RLS
був правильний і зелений — і пропустив зламаний вхід, бо перевіряв «чи
не видно чужого», коли зламалось «чи видно своє». Інтеграційні тести
grpcapi були зелені, бо не виконувались. Схема, довідник і протокол
описували те, чого в коді не існувало, і виглядало це як готове.

Тому в кожному завданні цих сесій стояла вимога назвати НЕПОКРИТЕ, а
чотири задачі закінчились не можливістю, а відмовою: правило з
нереалізованим джерелом не зберігається, профіль без команд заливки
каже про це замість мовчазної кнопки, міграція RLS валить сама себе на
таблиці без політики, тест словника аудиту падає на ключі без назви.

Подробиці — HISTORY.md, розділи за 26 і 27 серпня.
2026-08-27 17:32:49 +03:00

313 lines
17 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Повний стек NetPulse: БД, кеш, міграції, API з інтерфейсом, колектор
# зондів і зворотний проксі з автоматичним TLS.
#
# Швидкий старт:
# cp deploy/.env.example .env # і заповнити секрети
# docker compose up -d
# docker compose run --rm --entrypoint netpulse-user cli \
# -tenant default -login admin -role owner
#
# netpulse-user і netpulse-secret заводять кабінети, людей і секрети —
# тобто роблять рівно те, чого роль під RLS робити не має. Тому для них
# є окрема служба cli з DSN власника: під роллю застосунку вони не
# падають з помилкою, а мовчки нічого не знаходять.
#
# Тільки стенд для розробки (БД і кеш, решта — з go run):
# docker compose up -d db cache
name: netpulse
# ---------------------------------------------------------------------
# DSN трьох ролей
# ---------------------------------------------------------------------
#
# Три ролі, три різні відповіді на «що цій ролі вільно бачити» (0063).
#
# netpulse власник схеми. Ним ходять migrate і cli.
# netpulse_app API і колектор. Без BYPASSRLS — під політиками RLS.
# netpulse_worker фонові такти поверх усіх кабінетів. З BYPASSRLS.
#
# Заповнені NETPULSE_APP_PASSWORD і NETPULSE_WORKER_PASSWORD означають
# «застосунок ходить під RLS». На ЧИСТІЙ базі їх пише установник, а
# netpulse-migrate сам видає ці паролі ролям одразу після накочування
# схеми — тобто нова інсталяція працює під політиками з першої секунди,
# і слова «cutover» клієнт не бачить взагалі.
#
# Порожні означають «як до 0063»: DSN згортається до ролі власника,
# DSN воркера лишається порожнім, store.UseWorkerDSN другого пулу не
# відкриває. Саме на цьому тримається безпека оновлення наявного стенду:
# у його .env цих рядків немає, вирази нижче дають той самий DSN, що й
# раніше, а мігратор на непорожній базі паролів ролям не видає взагалі.
# Перехід для таких інсталяцій — deploy/RLS-EXISTING-INSTALL.md.
#
# Ім'я ролі більше НЕ окрема змінна, і це виправлення, а не спрощення.
# Раніше їх було дві — NETPULSE_APP_USER і NETPULSE_APP_PASSWORD, — і
# будь-яка половина без другої давала DSN, який не встановлюється: нова
# роль зі старим паролем або стара роль з новим. Виглядає це не як
# помилка конфігурації, а як «RLS усе зламав». Тепер прапорець один: є
# пароль — є й роль netpulse_app, немає пароля — немає й ролі.
#
# Вираз читається так. Перша частина додає «netpulse_app:» лише тоді,
# коли пароль заданий. Друга віддає або сам пароль (коли заданий), або
# цілу пару «netpulse:<пароль власника>» (коли ні). Обидві частини
# дивляться на ОДНУ змінну, тому проміжного стану не буває.
x-app-dsn: &app-dsn postgres://${NETPULSE_APP_PASSWORD:+netpulse_app:}${NETPULSE_APP_PASSWORD:-netpulse:$POSTGRES_PASSWORD}@db:5432/netpulse?sslmode=disable
x-worker-dsn: &worker-dsn ${NETPULSE_WORKER_PASSWORD:+postgres://netpulse_worker:${NETPULSE_WORKER_PASSWORD}@db:5432/netpulse?sslmode=disable}
x-owner-dsn: &owner-dsn postgres://netpulse:${POSTGRES_PASSWORD:?потрібен POSTGRES_PASSWORD}@db:5432/netpulse?sslmode=disable
x-server-env: &server-env
NETPULSE_DSN: *app-dsn
NETPULSE_DSN_WORKER: *worker-dsn
NETPULSE_DEK: ${NETPULSE_DEK:?потрібен NETPULSE_DEK — див. deploy/README.md}
NETPULSE_LOG_LEVEL: ${NETPULSE_LOG_LEVEL:-info}
TZ: ${TZ:-Europe/Kyiv}
services:
db:
image: timescale/timescaledb:2.17.2-pg16
environment:
POSTGRES_USER: netpulse
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?потрібен POSTGRES_PASSWORD}
POSTGRES_DB: netpulse
TIMESCALEDB_TELEMETRY: "off"
# Без цього initdb бере кодування з локалі середовища й цілком
# може створити базу як SQL_ASCII. Тексти при цьому виглядають
# цілими, а ламається інше: jsonb із екранованим не-ASCII не
# вставляється, а lower() і сортування працюють як над латиницею.
# Виправити можна лише перестворенням бази, тож задаємо одразу.
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C.UTF-8"
command:
- postgres
- -c
- shared_preload_libraries=timescaledb
- -c
- max_connections=200
- -c
- shared_buffers=${PG_SHARED_BUFFERS:-512MB}
- -c
# Вісім фонових робітників TimescaleDB — розумно на чотирьох
# ядрах і забагато на двох: вони конкурують за ті самі ядра з
# самими запитами, і стиснення чанків починає заважати опитуванню.
# Установник підставляє число за фактичною кількістю ядер.
- timescaledb.max_background_workers=${PG_BG_WORKERS:-8}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U netpulse -d netpulse"]
interval: 5s
timeout: 5s
retries: 20
restart: unless-stopped
cache:
image: docker.dragonflydb.io/dragonflydb/dragonfly:v1.25.5
# memlock без обмеження прискорює Dragonfly, але дозволений не всюди:
# у контейнерній віртуалізації (LXC, частина VPS) ядро відмовляє, і
# контейнер не стартує взагалі з «error setting rlimit type 8».
# Швидкість того не варта — обмеження знято.
#
# Стеля пам'яті задається явно, бо без неї Dragonfly бере її з
# доступної на машині — і рахує ту саму пам'ять, яку вже порахував
# PG_SHARED_BUFFERS. На машині з 4 ГБ два незалежні розрахунки
# чверті одного пирога закінчуються тим, що ядро вбиває когось
# третього, найімовірніше API. Установник задає обидві цифри разом.
command: ["--maxmemory=${DRAGONFLY_MAXMEMORY:-512mb}"]
volumes:
- cache-data:/data
restart: unless-stopped
# Міграції окремою службою, а не на старті API.
#
# API запускається в кількох примірниках, і накочування схеми зі старту
# означало б гонку між ними. Тут же — один запуск, який мусить
# завершитись успіхом, перш ніж піднімуться API й колектор.
migrate:
build: &server-build
context: .
dockerfile: deploy/Dockerfile.server
args:
VERSION: ${NETPULSE_VERSION:-dev}
COMMIT: ${NETPULSE_COMMIT:-none}
image: netpulse/server:${NETPULSE_VERSION:-dev}
entrypoint: ["netpulse-migrate"]
environment:
<<: *server-env
# Схему котить власник, а не netpulse_app: міграції створюють
# об'єкти, правлять самі політики й іноді переливають дані поверх
# кабінетів. Роль під RLS не змогла б жодного з трьох, а 0063 ще й
# перевіряє права netpulse_app — тобто мусить їх бачити.
NETPULSE_DSN: *owner-dsn
# А цими двома мігратор схему не котить — він ними ЗАХОДИТЬ.
#
# По-перше, на чистій базі саме звідси беруться паролі ролей:
# мігратор витягує пароль із самого DSN, тому в базу лягає рівно
# той рядок, який потім надішле pgx. Розійтись вони не можуть за
# побудовою — це той самий рядок і той самий розбирач.
#
# По-друге, перед тим як пустити api й collector, мігратор кожним
# з цих DSN підключається й дивиться, під якою роллю опинився.
# Саме цієї перевірки бракувало 0063: там перевіряли, що не видно
# чужого, а зламався вхід — тобто «чи видно своє».
NETPULSE_DSN_APP: *app-dsn
NETPULSE_DSN_WORKER: *worker-dsn
depends_on:
db:
condition: service_healthy
restart: "no"
# Утиліти командного рядка під роллю власника: netpulse-user (кабінети
# й люди), netpulse-secret (секрети в core.secrets).
#
# Окрема служба, а не `run --rm api`, бо DSN тут інший. Обидві утиліти
# роблять рівно те, чого роль під RLS робити не має: заводять кабінет
# (INSERT у core.tenants під політикою tenant_self неможливий за
# означенням) і додають людину в чужі за контекстом рядки. Під
# netpulse_app вони не падають, а мовчки нічого не знаходять — тобто
# на новій інсталяції перша ж команда «створити адміністратора»
# виглядала б як успіх, а адміністратора не було б.
#
# profiles: службу не піднімає ні `up`, ні `up -d`; `docker compose run`
# вмикає її профіль сам.
#
# docker compose run --rm --entrypoint netpulse-user cli \
# -tenant default -login admin -role owner
cli:
build: *server-build
image: netpulse/server:${NETPULSE_VERSION:-dev}
profiles: ["cli"]
environment:
<<: *server-env
NETPULSE_DSN: *owner-dsn
NETPULSE_DSN_WORKER: ""
volumes:
- git-data:/var/lib/netpulse
depends_on:
db:
condition: service_healthy
restart: "no"
api:
build: *server-build
image: netpulse/server:${NETPULSE_VERSION:-dev}
entrypoint: ["netpulse-api"]
command:
- -listen=:8080
- -alert-interval=${NETPULSE_ALERT_INTERVAL:-30s}
environment:
<<: *server-env
NETPULSE_JWT_SECRET: ${NETPULSE_JWT_SECRET:?потрібен NETPULSE_JWT_SECRET — не менше 32 байтів}
# Історія конфігів у Git. Спільний том для API й колектора:
# колектор туди пише під час бекапу, API звідти читає для порівняння
# версій. Різні томи означали б, що половина історії видна одному
# процесу, а половина другому.
volumes:
- git-data:/var/lib/netpulse
depends_on:
migrate:
condition: service_completed_successfully
cache:
condition: service_started
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
interval: 10s
timeout: 3s
retries: 6
start_period: 15s
restart: unless-stopped
# Колектор зондів. TLS знімає Caddy, всередині мережі — h2c, тому
# -insecure. Порт назовні сам не публікує: до нього ходять через
# проксі, який має справжній сертифікат.
collector:
build: *server-build
image: netpulse/server:${NETPULSE_VERSION:-dev}
entrypoint: ["netpulse-server"]
command:
- -listen=:9443
- -insecure
environment: *server-env
# Історія конфігів у Git. Спільний том для API й колектора:
# колектор туди пише під час бекапу, API звідти читає для порівняння
# версій. Різні томи означали б, що половина історії видна одному
# процесу, а половина другому.
volumes:
- git-data:/var/lib/netpulse
depends_on:
migrate:
condition: service_completed_successfully
restart: unless-stopped
proxy:
image: caddy:2.8-alpine
environment:
NETPULSE_DOMAIN: ${NETPULSE_DOMAIN:?потрібен NETPULSE_DOMAIN}
# Порожня пошта означає «домену ще немає» — тоді самопідписаний
# сертифікат. Інакше Caddy нескінченно проситиме Let's Encrypt
# видати сертифікат на IP, чого той не робить, і сайт лишиться
# без TLS зовсім.
NETPULSE_TLS: ${ACME_EMAIL:-internal}
volumes:
- ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
ports:
- "80:80"
- "443:443"
# Окремий порт для зондів: вони говорять gRPC, а не HTTP, і
# ділити з ним 443 означало б розрізняти протоколи за шляхом —
# зайва крихкість там, де порт коштує нічого.
- "9443:9443"
depends_on:
- api
- collector
restart: unless-stopped
# Зонд на тому ж хості, що й сервер: базовий моніторинг самої
# інсталяції. Профіль, а не звичайна служба: у типовому розгортанні
# зонди стоять у мережах клієнтів, а не поруч із сервером.
agent:
build:
context: .
dockerfile: deploy/Dockerfile.agent
args:
VERSION: ${NETPULSE_VERSION:-dev}
COMMIT: ${NETPULSE_COMMIT:-none}
image: netpulse/agent:${NETPULSE_VERSION:-dev}
profiles: ["agent"]
command:
- -server=collector:9443
- -insecure
- -modules=icmp,snmp,topology,ncm,traps
- -traps-listen=${NETPULSE_TRAPS_LISTEN:-:162}
environment:
NETPULSE_ENROLL: ${NETPULSE_ENROLL:-}
NETPULSE_NAME: ${NETPULSE_AGENT_NAME:-локальний зонд}
TZ: ${TZ:-Europe/Kyiv}
# Порт трапів назовні. Прив'язка задається змінною, а не зашита:
# 162/udp без автентифікації приймає будь-кого, хто знає адресу, і
# на машині з публічним інтерфейсом «усі інтерфейси» — це рішення, а
# не типове значення. Порожній NETPULSE_TRAPS_BIND лишає службу
# всередині мережі docker, і зовні вона недосяжна.
ports:
- "${NETPULSE_TRAPS_BIND:-127.0.0.1}:162:162/udp"
cap_add:
- NET_RAW
# 162 нижче 1024, а процес усередині — не root. Без цього зонд
# порт не займе; він про це кричить у журнал, але приймати трапи
# все одно не буде.
- NET_BIND_SERVICE
volumes:
- agent-identity:/var/lib/netpulse
depends_on:
- collector
restart: unless-stopped
volumes:
db-data:
git-data:
cache-data:
caddy-data:
caddy-config:
agent-identity: