Fail-fast secrets in stage/prod via EVENTHUB_ENV. Refs EventHub/EventHubBack#25
This commit is contained in:
+21
-4
@@ -1,4 +1,21 @@
|
|||||||
# .env.example — закоммитьте этот файл, а реальный .env держите в .gitignore
|
# Скопируйте в .env (файл .env в .gitignore). Для локальной разработки: EVENTHUB_ENV=dev
|
||||||
RELEASE_COOKIE=ваш-очень-длинный-секретный-куки
|
|
||||||
GRAFANA_ADMIN_PASSWORD=сложный-уникальный-пароль
|
# Режим: dev | stage | prod (stage/prod — обязательные сильные секреты)
|
||||||
JWT_SECRET=суперсекрет
|
EVENTHUB_ENV=dev
|
||||||
|
|
||||||
|
RELEASE_COOKIE=ваш-очень-длинный-секретный-куки-минимум-32-символа
|
||||||
|
GRAFANA_ADMIN_PASSWORD=сложный-уникальный-пароль
|
||||||
|
|
||||||
|
# JWT (минимум 32 символа, не из denylist — см. infra_secrets)
|
||||||
|
JWT_SECRET=замените-на-случайную-строку-минимум-32-символа-для-user-jwt
|
||||||
|
ADMIN_JWT_SECRET=замените-на-другую-случайную-строку-минимум-32-символа
|
||||||
|
|
||||||
|
# Seed админов (обязательны в stage/prod при пустой таблице admin; в dev — опционально)
|
||||||
|
ADMIN_SUPER_EMAIL=superadmin@eventhub.local
|
||||||
|
ADMIN_SUPER_PASSWORD=замените-сильный-пароль-суперадмина
|
||||||
|
ADMIN_EMAIL=admin@eventhub.local
|
||||||
|
ADMIN_PASSWORD=замените-сильный-пароль-админа
|
||||||
|
ADMIN_MODER_EMAIL=moderator@eventhub.local
|
||||||
|
ADMIN_MODER_PASSWORD=замените-сильный-пароль-модератора
|
||||||
|
ADMIN_SUPPORT_EMAIL=support@eventhub.local
|
||||||
|
ADMIN_SUPPORT_PASSWORD=замените-сильный-пароль-поддержки
|
||||||
|
|||||||
+14
-12
@@ -1,12 +1,14 @@
|
|||||||
RELEASE_COOKIE=FuNHSU3SIiW8ICu2XIbl0pvpxAhpBvO2Wki7CT6q2bpHDSlQ8p7gtUgjnyXpPJU5
|
EVENTHUB_ENV=stage
|
||||||
GRAFANA_ADMIN_PASSWORD=admin
|
RELEASE_COOKIE=FuNHSU3SIiW8ICu2XIbl0pvpxAhpBvO2Wki7CT6q2bpHDSlQ8p7gtUgjnyXpPJU5
|
||||||
JWT_SECRET=IDhKNFRLZFDYgPvVXwg4T7hAviQ2I1VGeEn5vXufQXK3AAErZuhMWy8J1uxJ0kGF
|
GRAFANA_ADMIN_PASSWORD=замените-сильный-пароль-grafana
|
||||||
TRAEFIK_LOG_PATH=/app/traefik/logs/access.log
|
JWT_SECRET=IDhKNFRLZFDYgPvVXwg4T7hAviQ2I1VGeEn5vXufQXK3AAErZuhMWy8J1uxJ0kGF
|
||||||
ADMIN_SUPER_EMAIL=superadmin@eventhub.local
|
ADMIN_JWT_SECRET=K3AAErZuhMWy8J1uxJ0kGFIDhKNFRLZFDYgPvVXwg4T7hAviQ2I1VGeEn5vXufQ
|
||||||
ADMIN_SUPER_PASSWORD=123456
|
TRAEFIK_LOG_PATH=/app/traefik/logs/access.log
|
||||||
ADMIN_EMAIL=admin@eventhub.local
|
ADMIN_SUPER_EMAIL=superadmin@eventhub.local
|
||||||
ADMIN_PASSWORD=123456
|
ADMIN_SUPER_PASSWORD=замените-сильный-пароль-суперадмина-мин32
|
||||||
ADMIN_MODER_EMAIL=moderator@eventhub.local
|
ADMIN_EMAIL=admin@eventhub.local
|
||||||
ADMIN_MODER_PASSWORD=123456
|
ADMIN_PASSWORD=замените-сильный-пароль-админа-минимум32
|
||||||
ADMIN_SUPPORT_EMAIL=support@eventhub.local
|
ADMIN_MODER_EMAIL=moderator@eventhub.local
|
||||||
ADMIN_SUPPORT_PASSWORD=123456
|
ADMIN_MODER_PASSWORD=замените-сильный-пароль-модератора-мин32
|
||||||
|
ADMIN_SUPPORT_EMAIL=support@eventhub.local
|
||||||
|
ADMIN_SUPPORT_PASSWORD=замените-сильный-пароль-поддержки-мин32
|
||||||
|
|||||||
@@ -1,81 +1,87 @@
|
|||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# ================== Балансировщик ==================
|
# ================== Балансировщик ==================
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:latest
|
image: traefik:latest
|
||||||
command:
|
command:
|
||||||
- "--log.level=DEBUG"
|
- "--log.level=DEBUG"
|
||||||
- "--api.insecure=true"
|
- "--api.insecure=true"
|
||||||
- "--providers.docker=true"
|
- "--providers.docker=true"
|
||||||
- "--providers.docker.exposedbydefault=false"
|
- "--providers.docker.exposedbydefault=false"
|
||||||
- "--entrypoints.web.address=:80"
|
- "--entrypoints.web.address=:80"
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
volumes:
|
volumes:
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||||
networks:
|
networks:
|
||||||
- eventhub-net
|
- eventhub-net
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: any
|
condition: any
|
||||||
|
|
||||||
# ================== Кластер EventHub ==================
|
# ================== Кластер EventHub ==================
|
||||||
eventhub:
|
eventhub:
|
||||||
image: git.sabilin.com/eventhub/eventhub:stage
|
image: git.sabilin.com/eventhub/eventhub:stage
|
||||||
hostname: "eventhub-node{{.Task.Slot}}"
|
hostname: "eventhub-node{{.Task.Slot}}"
|
||||||
environment:
|
environment:
|
||||||
- NODE_NAME=eventhub-node{{.Task.Slot}}
|
- EVENTHUB_ENV=stage
|
||||||
- RELEASE_COOKIE=${RELEASE_COOKIE}
|
- NODE_NAME=eventhub-node{{.Task.Slot}}
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
- RELEASE_COOKIE=${RELEASE_COOKIE}
|
||||||
- ADMIN_SUPER_EMAIL=${ADMIN_SUPER_EMAIL}
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
- ADMIN_SUPER_PASSWORD=${ADMIN_SUPER_PASSWORD}
|
- ADMIN_JWT_SECRET=${ADMIN_JWT_SECRET}
|
||||||
- ADMIN_EMAIL=${ADMIN_EMAIL}
|
- ADMIN_SUPER_EMAIL=${ADMIN_SUPER_EMAIL}
|
||||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
- ADMIN_SUPER_PASSWORD=${ADMIN_SUPER_PASSWORD}
|
||||||
- CLUSTER_MODE=true
|
- ADMIN_EMAIL=${ADMIN_EMAIL}
|
||||||
- DNS_NAME=eventhub-node
|
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
||||||
networks:
|
- ADMIN_MODER_EMAIL=${ADMIN_MODER_EMAIL}
|
||||||
eventhub-net:
|
- ADMIN_MODER_PASSWORD=${ADMIN_MODER_PASSWORD}
|
||||||
aliases:
|
- ADMIN_SUPPORT_EMAIL=${ADMIN_SUPPORT_EMAIL}
|
||||||
- eventhub-node
|
- ADMIN_SUPPORT_PASSWORD=${ADMIN_SUPPORT_PASSWORD}
|
||||||
volumes:
|
- CLUSTER_MODE=true
|
||||||
- eventhub-data:/app/data
|
- DNS_NAME=eventhub-node
|
||||||
healthcheck:
|
networks:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
eventhub-net:
|
||||||
interval: 10s
|
aliases:
|
||||||
timeout: 5s
|
- eventhub-node
|
||||||
retries: 3
|
volumes:
|
||||||
start_period: 30s
|
- eventhub-data:/app/data
|
||||||
deploy:
|
healthcheck:
|
||||||
replicas: 1
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||||
endpoint_mode: dnsrr
|
interval: 10s
|
||||||
restart_policy:
|
timeout: 5s
|
||||||
condition: any
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
# ================== Эмулятор ботов ==================
|
deploy:
|
||||||
bot-emulator-users:
|
replicas: 1
|
||||||
image: git.sabilin.com/eventhub/bot-emulator-users:stage
|
endpoint_mode: dnsrr
|
||||||
environment:
|
restart_policy:
|
||||||
- ADMIN_API_HOST=http://eventhub-node:8445
|
condition: any
|
||||||
- CLIENT_API_HOST=http://eventhub-node:8080
|
|
||||||
- ADMIN_EMAIL=${ADMIN_EMAIL}
|
# ================== Эмулятор ботов ==================
|
||||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
bot-emulator-users:
|
||||||
- BOT_PASSWORD=${BOT_PASSWORD}
|
image: git.sabilin.com/eventhub/bot-emulator-users:stage
|
||||||
- STARTUP_DELAY=30
|
environment:
|
||||||
- DEBUG=true
|
- ADMIN_API_HOST=http://eventhub-node:8445
|
||||||
networks:
|
- CLIENT_API_HOST=http://eventhub-node:8080
|
||||||
- eventhub-net
|
- ADMIN_EMAIL=${ADMIN_EMAIL}
|
||||||
depends_on:
|
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
||||||
- eventhub
|
- BOT_PASSWORD=${BOT_PASSWORD}
|
||||||
deploy:
|
- STARTUP_DELAY=30
|
||||||
replicas: 0
|
- DEBUG=true
|
||||||
restart_policy:
|
networks:
|
||||||
condition: any
|
- eventhub-net
|
||||||
|
depends_on:
|
||||||
networks:
|
- eventhub
|
||||||
eventhub-net:
|
deploy:
|
||||||
driver: overlay
|
replicas: 0
|
||||||
|
restart_policy:
|
||||||
volumes:
|
condition: any
|
||||||
|
|
||||||
|
networks:
|
||||||
|
eventhub-net:
|
||||||
|
driver: overlay
|
||||||
|
|
||||||
|
volumes:
|
||||||
eventhub-data:
|
eventhub-data:
|
||||||
+229
-225
@@ -1,226 +1,230 @@
|
|||||||
# docker/docker-compose.swarm.yml
|
# docker/docker-compose.swarm.yml
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# ================== Балансировщик ==================
|
# ================== Балансировщик ==================
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:latest
|
image: traefik:latest
|
||||||
command:
|
command:
|
||||||
# - "--log.level=DEBUG"
|
# - "--log.level=DEBUG"
|
||||||
- "--api.insecure=true"
|
- "--api.insecure=true"
|
||||||
- "--providers.docker=true"
|
- "--providers.docker=true"
|
||||||
# - "--providers.swarm.endpoint=unix:///var/run/docker.sock" # провайдер Swarm
|
# - "--providers.swarm.endpoint=unix:///var/run/docker.sock" # провайдер Swarm
|
||||||
- "--providers.docker.exposedbydefault=false"
|
- "--providers.docker.exposedbydefault=false"
|
||||||
- "--providers.file.filename=/etc/traefik/dynamic_conf.yml"
|
- "--providers.file.filename=/etc/traefik/dynamic_conf.yml"
|
||||||
- "--entrypoints.web.address=:80"
|
- "--entrypoints.web.address=:80"
|
||||||
- "--entrypoints.websecure.address=:443"
|
- "--entrypoints.websecure.address=:443"
|
||||||
- "--metrics.prometheus=true"
|
- "--metrics.prometheus=true"
|
||||||
- "--metrics.prometheus.buckets=0.1,0.3,1.2,5.0"
|
- "--metrics.prometheus.buckets=0.1,0.3,1.2,5.0"
|
||||||
- "--metrics.prometheus.addEntryPointsLabels=true"
|
- "--metrics.prometheus.addEntryPointsLabels=true"
|
||||||
- "--metrics.prometheus.addServicesLabels=true"
|
- "--metrics.prometheus.addServicesLabels=true"
|
||||||
- "--accesslog=true"
|
- "--accesslog=true"
|
||||||
- "--accesslog.filepath=/var/log/traefik/access.log"
|
- "--accesslog.filepath=/var/log/traefik/access.log"
|
||||||
- "--accesslog.format=json"
|
- "--accesslog.format=json"
|
||||||
- "--experimental.plugins.coraza.modulename=github.com/jcchavezs/coraza-http-wasm-traefik"
|
- "--experimental.plugins.coraza.modulename=github.com/jcchavezs/coraza-http-wasm-traefik"
|
||||||
- "--experimental.plugins.coraza.version=v0.3.0"
|
- "--experimental.plugins.coraza.version=v0.3.0"
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||||
- "./traefik/certs:/etc/traefik/certs:ro"
|
- "./traefik/certs:/etc/traefik/certs:ro"
|
||||||
- "./traefik/dynamic_conf.yml:/etc/traefik/dynamic_conf.yml:ro"
|
- "./traefik/dynamic_conf.yml:/etc/traefik/dynamic_conf.yml:ro"
|
||||||
- "traefik-logs:/var/log/traefik"
|
- "traefik-logs:/var/log/traefik"
|
||||||
- "traefik-plugins:/plugins-storage"
|
- "traefik-plugins:/plugins-storage"
|
||||||
networks:
|
networks:
|
||||||
- eventhub-net
|
- eventhub-net
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: any
|
condition: any
|
||||||
|
|
||||||
# ================== Сервис заглушка ==================
|
# ================== Сервис заглушка ==================
|
||||||
fallback:
|
fallback:
|
||||||
image: fallback:latest
|
image: fallback:latest
|
||||||
networks:
|
networks:
|
||||||
- eventhub-net
|
- eventhub-net
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 0
|
replicas: 0
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: any
|
condition: any
|
||||||
|
|
||||||
# ================== Кластер EventHub ==================
|
# ================== Кластер EventHub ==================
|
||||||
eventhub:
|
eventhub:
|
||||||
image: eventhub:latest
|
image: eventhub:latest
|
||||||
hostname: "eventhub-node{{.Task.Slot}}"
|
hostname: "eventhub-node{{.Task.Slot}}"
|
||||||
environment:
|
environment:
|
||||||
- NODE_NAME=eventhub-node{{.Task.Slot}}
|
- EVENTHUB_ENV=prod
|
||||||
- RELEASE_COOKIE=${RELEASE_COOKIE:-eventhub_cookie}
|
- NODE_NAME=eventhub-node{{.Task.Slot}}
|
||||||
- JWT_SECRET=${JWT_SECRET:-eventhub_top_secret}
|
- RELEASE_COOKIE=${RELEASE_COOKIE}
|
||||||
- ADMIN_SUPER_EMAIL=${ADMIN_SUPER_EMAIL:-superadmin@eventhub.local}
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
- ADMIN_SUPER_PASSWORD=${ADMIN_SUPER_PASSWORD:-123456}
|
- ADMIN_JWT_SECRET=${ADMIN_JWT_SECRET}
|
||||||
- ADMIN_MODER_EMAIL=${ADMIN_MODER_EMAIL:-moderator@eventhub.local}
|
- ADMIN_SUPER_EMAIL=${ADMIN_SUPER_EMAIL}
|
||||||
- ADMIN_MODER_PASSWORD=${ADMIN_MODER_PASSWORD:-123456}
|
- ADMIN_SUPER_PASSWORD=${ADMIN_SUPER_PASSWORD}
|
||||||
- ADMIN_SUPPORT_EMAIL=${ADMIN_SUPPORT_EMAIL:-support@eventhub.local}
|
- ADMIN_EMAIL=${ADMIN_EMAIL}
|
||||||
- ADMIN_SUPPORT_PASSWORD=${ADMIN_SUPPORT_PASSWORD:-123456}
|
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
||||||
- CLUSTER_MODE=true
|
- ADMIN_MODER_EMAIL=${ADMIN_MODER_EMAIL}
|
||||||
- DNS_NAME=eventhub-node
|
- ADMIN_MODER_PASSWORD=${ADMIN_MODER_PASSWORD}
|
||||||
networks:
|
- ADMIN_SUPPORT_EMAIL=${ADMIN_SUPPORT_EMAIL}
|
||||||
eventhub-net:
|
- ADMIN_SUPPORT_PASSWORD=${ADMIN_SUPPORT_PASSWORD}
|
||||||
aliases:
|
- CLUSTER_MODE=true
|
||||||
- eventhub-node
|
- DNS_NAME=eventhub-node
|
||||||
volumes:
|
networks:
|
||||||
- type: volume
|
eventhub-net:
|
||||||
source: eventhub-data
|
aliases:
|
||||||
target: /app/data
|
- eventhub-node
|
||||||
deploy:
|
volumes:
|
||||||
replicas: 1
|
- type: volume
|
||||||
endpoint_mode: dnsrr
|
source: eventhub-data
|
||||||
restart_policy:
|
target: /app/data
|
||||||
condition: any
|
deploy:
|
||||||
labels:
|
replicas: 1
|
||||||
- "traefik.enable=true"
|
endpoint_mode: dnsrr
|
||||||
|
restart_policy:
|
||||||
# ================== Admin UI ==================
|
condition: any
|
||||||
admin-ui:
|
labels:
|
||||||
image: eventhub-admin-ui:latest
|
- "traefik.enable=true"
|
||||||
networks:
|
|
||||||
- eventhub-net
|
# ================== Admin UI ==================
|
||||||
depends_on:
|
admin-ui:
|
||||||
- eventhub
|
image: eventhub-admin-ui:latest
|
||||||
deploy:
|
networks:
|
||||||
replicas: 1
|
- eventhub-net
|
||||||
restart_policy:
|
depends_on:
|
||||||
condition: any
|
- eventhub
|
||||||
labels:
|
deploy:
|
||||||
- "traefik.enable=true"
|
replicas: 1
|
||||||
|
restart_policy:
|
||||||
# ================== Мониторинг ==================
|
condition: any
|
||||||
prometheus:
|
labels:
|
||||||
image: prom/prometheus:latest
|
- "traefik.enable=true"
|
||||||
command:
|
|
||||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
# ================== Мониторинг ==================
|
||||||
- '--storage.tsdb.path=/prometheus'
|
prometheus:
|
||||||
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
|
image: prom/prometheus:latest
|
||||||
- '--web.console.templates=/usr/share/prometheus/consoles'
|
command:
|
||||||
- '--storage.tsdb.retention.time=30d'
|
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||||
- '--storage.tsdb.retention.size=15GB'
|
- '--storage.tsdb.path=/prometheus'
|
||||||
volumes:
|
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
|
||||||
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
|
- '--web.console.templates=/usr/share/prometheus/consoles'
|
||||||
- prometheus-data:/prometheus
|
- '--storage.tsdb.retention.time=30d'
|
||||||
networks:
|
- '--storage.tsdb.retention.size=15GB'
|
||||||
- eventhub-net
|
volumes:
|
||||||
ports:
|
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
- "9090:9090"
|
- prometheus-data:/prometheus
|
||||||
deploy:
|
networks:
|
||||||
replicas: 0
|
- eventhub-net
|
||||||
restart_policy:
|
ports:
|
||||||
condition: any
|
- "9090:9090"
|
||||||
|
deploy:
|
||||||
grafana:
|
replicas: 0
|
||||||
image: grafana/grafana:latest
|
restart_policy:
|
||||||
environment:
|
condition: any
|
||||||
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
|
|
||||||
- GF_SECURITY_DISABLE_INITIAL_ADMIN_PASSWORD_CHANGE=false
|
grafana:
|
||||||
- GF_USERS_ALLOW_SIGN_UP=false
|
image: grafana/grafana:latest
|
||||||
- GF_AUTH_ANONYMOUS_ENABLED=false
|
environment:
|
||||||
volumes:
|
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
|
||||||
- ./grafana/provisioning:/etc/grafana/provisioning
|
- GF_SECURITY_DISABLE_INITIAL_ADMIN_PASSWORD_CHANGE=false
|
||||||
- ./grafana/dashboards:/etc/grafana/dashboards
|
- GF_USERS_ALLOW_SIGN_UP=false
|
||||||
- grafana-data:/var/lib/grafana
|
- GF_AUTH_ANONYMOUS_ENABLED=false
|
||||||
networks:
|
volumes:
|
||||||
- eventhub-net
|
- ./grafana/provisioning:/etc/grafana/provisioning
|
||||||
ports:
|
- ./grafana/dashboards:/etc/grafana/dashboards
|
||||||
- "3000:3000"
|
- grafana-data:/var/lib/grafana
|
||||||
deploy:
|
networks:
|
||||||
replicas: 0
|
- eventhub-net
|
||||||
restart_policy:
|
ports:
|
||||||
condition: any
|
- "3000:3000"
|
||||||
|
deploy:
|
||||||
# ================== Аналитика логов ==================
|
replicas: 0
|
||||||
loglynx:
|
restart_policy:
|
||||||
image: k0lin/loglynx:latest
|
condition: any
|
||||||
user: root
|
|
||||||
ports:
|
# ================== Аналитика логов ==================
|
||||||
- "6123:6123"
|
loglynx:
|
||||||
volumes:
|
image: k0lin/loglynx:latest
|
||||||
- traefik-logs:/app/traefik/logs:ro
|
user: root
|
||||||
- loglynx-data:/app/data
|
ports:
|
||||||
environment:
|
- "6123:6123"
|
||||||
- TRAEFIK_LOG_PATH=${TRAEFIK_LOG_PATH}
|
volumes:
|
||||||
- SERVER_PORT=6123
|
- traefik-logs:/app/traefik/logs:ro
|
||||||
- DATABASE_PATH=/app/data/loglynx.db
|
- loglynx-data:/app/data
|
||||||
networks:
|
environment:
|
||||||
- eventhub-net
|
- TRAEFIK_LOG_PATH=${TRAEFIK_LOG_PATH}
|
||||||
deploy:
|
- SERVER_PORT=6123
|
||||||
replicas: 0
|
- DATABASE_PATH=/app/data/loglynx.db
|
||||||
restart_policy:
|
networks:
|
||||||
condition: any
|
- eventhub-net
|
||||||
|
deploy:
|
||||||
# ================== Инструмент отладки ==================
|
replicas: 0
|
||||||
observer_web:
|
restart_policy:
|
||||||
image: observer_web:latest
|
condition: any
|
||||||
environment:
|
|
||||||
- RELEASE_COOKIE=${RELEASE_COOKIE}
|
# ================== Инструмент отладки ==================
|
||||||
networks:
|
observer_web:
|
||||||
- eventhub-net
|
image: observer_web:latest
|
||||||
ports:
|
environment:
|
||||||
- "4000:4000"
|
- RELEASE_COOKIE=${RELEASE_COOKIE}
|
||||||
deploy:
|
networks:
|
||||||
replicas: 0
|
- eventhub-net
|
||||||
restart_policy:
|
ports:
|
||||||
condition: any
|
- "4000:4000"
|
||||||
|
deploy:
|
||||||
# ================== Ротация логов ==================
|
replicas: 0
|
||||||
logrotate:
|
restart_policy:
|
||||||
image: logrotate:latest
|
condition: any
|
||||||
volumes:
|
|
||||||
- traefik-logs:/var/log/traefik:rw
|
# ================== Ротация логов ==================
|
||||||
networks:
|
logrotate:
|
||||||
- eventhub-net
|
image: logrotate:latest
|
||||||
deploy:
|
volumes:
|
||||||
replicas: 0
|
- traefik-logs:/var/log/traefik:rw
|
||||||
restart_policy:
|
networks:
|
||||||
condition: any
|
- eventhub-net
|
||||||
|
deploy:
|
||||||
bot-emulator-users:
|
replicas: 0
|
||||||
image: bot-emulator-users:latest
|
restart_policy:
|
||||||
environment:
|
condition: any
|
||||||
- ADMIN_API_HOST=http://eventhub-node:8445
|
|
||||||
- CLIENT_API_HOST=http://eventhub-node:8080
|
bot-emulator-users:
|
||||||
- ADMIN_EMAIL=admin@eventhub.local
|
image: bot-emulator-users:latest
|
||||||
- ADMIN_PASSWORD=123456
|
environment:
|
||||||
- BOT_PASSWORD=botpass123
|
- ADMIN_API_HOST=http://eventhub-node:8445
|
||||||
- MIN_DELAY=0.5
|
- CLIENT_API_HOST=http://eventhub-node:8080
|
||||||
- MAX_DELAY=3.0
|
- ADMIN_EMAIL=admin@eventhub.local
|
||||||
- LOOP_FOREVER=true
|
- ADMIN_PASSWORD=123456
|
||||||
- BOT_REFRESH_INTERVAL=300
|
- BOT_PASSWORD=botpass123
|
||||||
- TICKET_CHANCE=0.07
|
- MIN_DELAY=0.5
|
||||||
- STARTUP_DELAY=30
|
- MAX_DELAY=3.0
|
||||||
- DEBUG=false
|
- LOOP_FOREVER=true
|
||||||
networks:
|
- BOT_REFRESH_INTERVAL=300
|
||||||
- eventhub-net
|
- TICKET_CHANCE=0.07
|
||||||
depends_on:
|
- STARTUP_DELAY=30
|
||||||
- eventhub
|
- DEBUG=false
|
||||||
deploy:
|
networks:
|
||||||
mode: replicated
|
- eventhub-net
|
||||||
replicas: 1
|
depends_on:
|
||||||
restart_policy:
|
- eventhub
|
||||||
condition: any
|
deploy:
|
||||||
delay: 5s
|
mode: replicated
|
||||||
max_attempts: 3
|
replicas: 1
|
||||||
window: 120s
|
restart_policy:
|
||||||
|
condition: any
|
||||||
networks:
|
delay: 5s
|
||||||
eventhub-net:
|
max_attempts: 3
|
||||||
driver: overlay
|
window: 120s
|
||||||
|
|
||||||
volumes:
|
networks:
|
||||||
eventhub-data:
|
eventhub-net:
|
||||||
# name: 'eventhub-data-{{.Task.Slot}}'
|
driver: overlay
|
||||||
prometheus-data:
|
|
||||||
grafana-data:
|
volumes:
|
||||||
traefik-logs:
|
eventhub-data:
|
||||||
loglynx-data:
|
# name: 'eventhub-data-{{.Task.Slot}}'
|
||||||
|
prometheus-data:
|
||||||
|
grafana-data:
|
||||||
|
traefik-logs:
|
||||||
|
loglynx-data:
|
||||||
traefik-plugins:
|
traefik-plugins:
|
||||||
+218
-212
@@ -1,213 +1,219 @@
|
|||||||
# docker/docker-compose.yml
|
# docker/docker-compose.yml
|
||||||
services:
|
services:
|
||||||
# ================== Балансировщик (HTTPS/WSS, WAF, логи) ==================
|
# ================== Балансировщик (HTTPS/WSS, WAF, логи) ==================
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:latest
|
image: traefik:latest
|
||||||
user: "0:1001" # группа docker на хосте
|
user: "0:1001" # группа docker на хосте
|
||||||
command:
|
command:
|
||||||
- "--api.insecure=true"
|
- "--api.insecure=true"
|
||||||
- "--providers.docker=true"
|
- "--providers.docker=true"
|
||||||
- "--providers.docker.exposedbydefault=false"
|
- "--providers.docker.exposedbydefault=false"
|
||||||
- "--providers.file.filename=/etc/traefik/dynamic_conf.yml"
|
- "--providers.file.filename=/etc/traefik/dynamic_conf.yml"
|
||||||
- "--entrypoints.web.address=:80"
|
- "--entrypoints.web.address=:80"
|
||||||
- "--entrypoints.websecure.address=:443"
|
- "--entrypoints.websecure.address=:443"
|
||||||
# Метрики
|
# Метрики
|
||||||
- "--metrics.prometheus=true"
|
- "--metrics.prometheus=true"
|
||||||
- "--metrics.prometheus.buckets=0.1,0.3,1.2,5.0"
|
- "--metrics.prometheus.buckets=0.1,0.3,1.2,5.0"
|
||||||
- "--metrics.prometheus.addEntryPointsLabels=true"
|
- "--metrics.prometheus.addEntryPointsLabels=true"
|
||||||
- "--metrics.prometheus.addServicesLabels=true"
|
- "--metrics.prometheus.addServicesLabels=true"
|
||||||
# Логи доступа (JSON)
|
# Логи доступа (JSON)
|
||||||
- "--accesslog=true"
|
- "--accesslog=true"
|
||||||
- "--accesslog.filepath=/var/log/traefik/access.log"
|
- "--accesslog.filepath=/var/log/traefik/access.log"
|
||||||
- "--accesslog.format=json"
|
- "--accesslog.format=json"
|
||||||
# Coraza WAF
|
# Coraza WAF
|
||||||
- "--experimental.plugins.coraza.modulename=github.com/jcchavezs/coraza-http-wasm-traefik"
|
- "--experimental.plugins.coraza.modulename=github.com/jcchavezs/coraza-http-wasm-traefik"
|
||||||
- "--experimental.plugins.coraza.version=v0.2.0"
|
- "--experimental.plugins.coraza.version=v0.2.0"
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
# порт дашборда (опционально)
|
# порт дашборда (опционально)
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||||
- "./traefik/certs:/etc/traefik/certs:ro"
|
- "./traefik/certs:/etc/traefik/certs:ro"
|
||||||
- "./traefik/dynamic_conf.yml:/etc/traefik/dynamic_conf.yml:ro"
|
- "./traefik/dynamic_conf.yml:/etc/traefik/dynamic_conf.yml:ro"
|
||||||
- "traefik-logs:/var/log/traefik" # для LogLynx
|
- "traefik-logs:/var/log/traefik" # для LogLynx
|
||||||
networks:
|
networks:
|
||||||
- eventhub-net
|
- eventhub-net
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# ================== Сервис-заглушка (Failover) ==================
|
# ================== Сервис-заглушка (Failover) ==================
|
||||||
fallback:
|
fallback:
|
||||||
build:
|
build:
|
||||||
context: ./fallback
|
context: ./fallback
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
networks:
|
networks:
|
||||||
- eventhub-net
|
- eventhub-net
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
profiles: ['ci']
|
profiles: ['ci']
|
||||||
|
|
||||||
# ================== Кластер EventHub (3 ноды) ==================
|
# ================== Кластер EventHub (3 ноды) ==================
|
||||||
eventhub-node1:
|
eventhub-node1:
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: docker/Dockerfile
|
dockerfile: docker/Dockerfile
|
||||||
hostname: eventhub-node1.local
|
hostname: eventhub-node1.local
|
||||||
environment:
|
environment:
|
||||||
- NODE_NAME=eventhub-node1@eventhub-node1.local
|
- EVENTHUB_ENV=dev
|
||||||
- RELEASE_COOKIE=${RELEASE_COOKIE}
|
- NODE_NAME=eventhub-node1@eventhub-node1.local
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
- RELEASE_COOKIE=${RELEASE_COOKIE}
|
||||||
networks:
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
eventhub-net:
|
- ADMIN_JWT_SECRET=${ADMIN_JWT_SECRET}
|
||||||
aliases:
|
networks:
|
||||||
- eventhub-node # ← общий алиас для DNS-лукапа
|
eventhub-net:
|
||||||
volumes:
|
aliases:
|
||||||
- eventhub-node1-data:/app/data
|
- eventhub-node # ← общий алиас для DNS-лукапа
|
||||||
labels:
|
volumes:
|
||||||
- "traefik.enable=true"
|
- eventhub-node1-data:/app/data
|
||||||
restart: unless-stopped
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
eventhub-node2:
|
restart: unless-stopped
|
||||||
build:
|
|
||||||
context: ..
|
eventhub-node2:
|
||||||
dockerfile: docker/Dockerfile
|
build:
|
||||||
hostname: eventhub-node2.local
|
context: ..
|
||||||
environment:
|
dockerfile: docker/Dockerfile
|
||||||
- NODE_NAME=eventhub-node2@eventhub-node2.local
|
hostname: eventhub-node2.local
|
||||||
- RELEASE_COOKIE=${RELEASE_COOKIE}
|
environment:
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
- EVENTHUB_ENV=dev
|
||||||
networks:
|
- NODE_NAME=eventhub-node2@eventhub-node2.local
|
||||||
eventhub-net:
|
- RELEASE_COOKIE=${RELEASE_COOKIE}
|
||||||
aliases:
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
- eventhub-node # ← общий алиас для DNS-лукапа
|
- ADMIN_JWT_SECRET=${ADMIN_JWT_SECRET}
|
||||||
volumes:
|
networks:
|
||||||
- eventhub-node2-data:/app/data
|
eventhub-net:
|
||||||
labels:
|
aliases:
|
||||||
- "traefik.enable=true"
|
- eventhub-node # ← общий алиас для DNS-лукапа
|
||||||
restart: unless-stopped
|
volumes:
|
||||||
|
- eventhub-node2-data:/app/data
|
||||||
eventhub-node3:
|
labels:
|
||||||
build:
|
- "traefik.enable=true"
|
||||||
context: ..
|
restart: unless-stopped
|
||||||
dockerfile: docker/Dockerfile
|
|
||||||
hostname: eventhub-node3.local
|
eventhub-node3:
|
||||||
environment:
|
build:
|
||||||
- NODE_NAME=eventhub-node3@eventhub-node3.local
|
context: ..
|
||||||
- RELEASE_COOKIE=${RELEASE_COOKIE}
|
dockerfile: docker/Dockerfile
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
hostname: eventhub-node3.local
|
||||||
networks:
|
environment:
|
||||||
eventhub-net:
|
- EVENTHUB_ENV=dev
|
||||||
aliases:
|
- NODE_NAME=eventhub-node3@eventhub-node3.local
|
||||||
- eventhub-node # ← общий алиас для DNS-лукапа
|
- RELEASE_COOKIE=${RELEASE_COOKIE}
|
||||||
volumes:
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
- eventhub-node3-data:/app/data
|
- ADMIN_JWT_SECRET=${ADMIN_JWT_SECRET}
|
||||||
labels:
|
networks:
|
||||||
- "traefik.enable=true"
|
eventhub-net:
|
||||||
restart: unless-stopped
|
aliases:
|
||||||
|
- eventhub-node # ← общий алиас для DNS-лукапа
|
||||||
# ================== Admin ui ==================
|
volumes:
|
||||||
admin-ui:
|
- eventhub-node3-data:/app/data
|
||||||
build:
|
labels:
|
||||||
context: ../../EventHubFrontAdmin # путь к проекту админки
|
- "traefik.enable=true"
|
||||||
dockerfile: Dockerfile
|
restart: unless-stopped
|
||||||
networks:
|
|
||||||
- eventhub-net
|
# ================== Admin ui ==================
|
||||||
# restart: unless-stopped
|
admin-ui:
|
||||||
restart: no
|
build:
|
||||||
labels:
|
context: ../../EventHubFrontAdmin # путь к проекту админки
|
||||||
- "traefik.enable=true"
|
dockerfile: Dockerfile
|
||||||
profiles: ['ui']
|
networks:
|
||||||
# ================== Мониторинг ==================
|
- eventhub-net
|
||||||
prometheus:
|
# restart: unless-stopped
|
||||||
image: prom/prometheus:latest
|
restart: no
|
||||||
command:
|
labels:
|
||||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
- "traefik.enable=true"
|
||||||
- '--storage.tsdb.path=/prometheus'
|
profiles: ['ui']
|
||||||
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
|
# ================== Мониторинг ==================
|
||||||
- '--web.console.templates=/usr/share/prometheus/consoles'
|
prometheus:
|
||||||
- '--storage.tsdb.retention.time=30d'
|
image: prom/prometheus:latest
|
||||||
- '--storage.tsdb.retention.size=15GB'
|
command:
|
||||||
volumes:
|
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||||
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
|
- '--storage.tsdb.path=/prometheus'
|
||||||
- prometheus-data:/prometheus
|
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
|
||||||
networks:
|
- '--web.console.templates=/usr/share/prometheus/consoles'
|
||||||
- eventhub-net
|
- '--storage.tsdb.retention.time=30d'
|
||||||
ports:
|
- '--storage.tsdb.retention.size=15GB'
|
||||||
- "9090:9090"
|
volumes:
|
||||||
restart: unless-stopped
|
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
profiles: ['ci']
|
- prometheus-data:/prometheus
|
||||||
|
networks:
|
||||||
grafana:
|
- eventhub-net
|
||||||
image: grafana/grafana:latest
|
ports:
|
||||||
environment:
|
- "9090:9090"
|
||||||
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
|
restart: unless-stopped
|
||||||
- GF_SECURITY_DISABLE_INITIAL_ADMIN_PASSWORD_CHANGE=false
|
profiles: ['ci']
|
||||||
- GF_USERS_ALLOW_SIGN_UP=false
|
|
||||||
- GF_AUTH_ANONYMOUS_ENABLED=false
|
grafana:
|
||||||
volumes:
|
image: grafana/grafana:latest
|
||||||
- ./grafana/provisioning:/etc/grafana/provisioning
|
environment:
|
||||||
- ./grafana/dashboards:/etc/grafana/dashboards
|
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
|
||||||
- grafana-data:/var/lib/grafana
|
- GF_SECURITY_DISABLE_INITIAL_ADMIN_PASSWORD_CHANGE=false
|
||||||
networks:
|
- GF_USERS_ALLOW_SIGN_UP=false
|
||||||
- eventhub-net
|
- GF_AUTH_ANONYMOUS_ENABLED=false
|
||||||
ports:
|
volumes:
|
||||||
- "3000:3000"
|
- ./grafana/provisioning:/etc/grafana/provisioning
|
||||||
restart: unless-stopped
|
- ./grafana/dashboards:/etc/grafana/dashboards
|
||||||
profiles: ['ci']
|
- grafana-data:/var/lib/grafana
|
||||||
|
networks:
|
||||||
# ================== Аналитика логов ==================
|
- eventhub-net
|
||||||
loglynx:
|
ports:
|
||||||
image: k0lin/loglynx:latest
|
- "3000:3000"
|
||||||
user: root
|
restart: unless-stopped
|
||||||
restart: unless-stopped
|
profiles: ['ci']
|
||||||
ports:
|
|
||||||
- "6123:6123"
|
# ================== Аналитика логов ==================
|
||||||
volumes:
|
loglynx:
|
||||||
- traefik-logs:/app/traefik/logs:ro
|
image: k0lin/loglynx:latest
|
||||||
- loglynx-data:/app/data
|
user: root
|
||||||
environment:
|
restart: unless-stopped
|
||||||
- TRAEFIK_LOG_PATH=${TRAEFIK_LOG_PATH}
|
ports:
|
||||||
- SERVER_PORT=6123
|
- "6123:6123"
|
||||||
- DATABASE_PATH=/app/data/loglynx.db
|
volumes:
|
||||||
networks:
|
- traefik-logs:/app/traefik/logs:ro
|
||||||
- eventhub-net
|
- loglynx-data:/app/data
|
||||||
profiles: ['ci']
|
environment:
|
||||||
|
- TRAEFIK_LOG_PATH=${TRAEFIK_LOG_PATH}
|
||||||
# ================== Инструмент отладки ==================
|
- SERVER_PORT=6123
|
||||||
observer_web:
|
- DATABASE_PATH=/app/data/loglynx.db
|
||||||
build:
|
networks:
|
||||||
context: ..
|
- eventhub-net
|
||||||
dockerfile: docker/ObserverWeb.Dockerfile
|
profiles: ['ci']
|
||||||
environment:
|
|
||||||
- RELEASE_COOKIE=${RELEASE_COOKIE}
|
# ================== Инструмент отладки ==================
|
||||||
networks:
|
observer_web:
|
||||||
- eventhub-net
|
build:
|
||||||
ports:
|
context: ..
|
||||||
- "4000:4000"
|
dockerfile: docker/ObserverWeb.Dockerfile
|
||||||
restart: unless-stopped
|
environment:
|
||||||
profiles: ['ci']
|
- RELEASE_COOKIE=${RELEASE_COOKIE}
|
||||||
|
networks:
|
||||||
# ================== Ротация логов Traefik ==================
|
- eventhub-net
|
||||||
logrotate:
|
ports:
|
||||||
build:
|
- "4000:4000"
|
||||||
context: ./logrotate
|
restart: unless-stopped
|
||||||
dockerfile: Dockerfile
|
profiles: ['ci']
|
||||||
volumes:
|
|
||||||
- traefik-logs:/var/log/traefik:rw
|
# ================== Ротация логов Traefik ==================
|
||||||
networks:
|
logrotate:
|
||||||
- eventhub-net
|
build:
|
||||||
restart: unless-stopped
|
context: ./logrotate
|
||||||
profiles: ['ci']
|
dockerfile: Dockerfile
|
||||||
|
volumes:
|
||||||
networks:
|
- traefik-logs:/var/log/traefik:rw
|
||||||
eventhub-net:
|
networks:
|
||||||
driver: bridge
|
- eventhub-net
|
||||||
|
restart: unless-stopped
|
||||||
volumes:
|
profiles: ['ci']
|
||||||
eventhub-node1-data:
|
|
||||||
eventhub-node2-data:
|
networks:
|
||||||
eventhub-node3-data:
|
eventhub-net:
|
||||||
prometheus-data:
|
driver: bridge
|
||||||
grafana-data:
|
|
||||||
traefik-logs:
|
volumes:
|
||||||
|
eventhub-node1-data:
|
||||||
|
eventhub-node2-data:
|
||||||
|
eventhub-node3-data:
|
||||||
|
prometheus-data:
|
||||||
|
grafana-data:
|
||||||
|
traefik-logs:
|
||||||
loglynx-data:
|
loglynx-data:
|
||||||
+23
-22
@@ -1,23 +1,24 @@
|
|||||||
[
|
[
|
||||||
{eventhub, [
|
{eventhub, [
|
||||||
{http_port, "${HTTP_PORT:-8080}"},
|
{http_port, "${HTTP_PORT:-8080}"},
|
||||||
{ws_port, "${WS_PORT:-8081}"},
|
{ws_port, "${WS_PORT:-8081}"},
|
||||||
{admin_http_port, "${ADMIN_HTTP_PORT:-8445}"},
|
{admin_http_port, "${ADMIN_HTTP_PORT:-8445}"},
|
||||||
{admin_ws_port, "${ADMIN_WS_PORT:-8446}"},
|
{admin_ws_port, "${ADMIN_WS_PORT:-8446}"},
|
||||||
{jwt_secret, <<"${JWT_SECRET:-change_me_in_production}">>}
|
{jwt_secret, <<"${JWT_SECRET:-change_me_in_production}">>},
|
||||||
]},
|
{admin_jwt_secret, <<"${ADMIN_JWT_SECRET:-change_me_in_production}">>}
|
||||||
{kernel, [
|
]},
|
||||||
{logger_level, info},
|
{kernel, [
|
||||||
{logger, [
|
{logger_level, info},
|
||||||
{handler, default, logger_std_h,
|
{logger, [
|
||||||
#{formatter => {logger_formatter, #{template => [time, " ", level, ": ", msg, "\n"]}}}
|
{handler, default, logger_std_h,
|
||||||
}
|
#{formatter => {logger_formatter, #{template => [time, " ", level, ": ", msg, "\n"]}}}
|
||||||
]}
|
}
|
||||||
]},
|
]}
|
||||||
{mnesia, [
|
]},
|
||||||
{dir, "data/Mnesia.eventhub@${NODE_NAME}"}
|
{mnesia, [
|
||||||
]},
|
{dir, "data/Mnesia.eventhub@${NODE_NAME}"}
|
||||||
{ cowboy_swagger, [
|
]},
|
||||||
{ static_files, "./_build/default/lib/cowboy_swagger/priv/swagger" }
|
{ cowboy_swagger, [
|
||||||
]}
|
{ static_files, "./_build/default/lib/cowboy_swagger/priv/swagger" }
|
||||||
|
]}
|
||||||
].
|
].
|
||||||
+55
-26
@@ -3,6 +3,15 @@
|
|||||||
-export([start/2, stop/1]).
|
-export([start/2, stop/1]).
|
||||||
|
|
||||||
start(_StartType, _StartArgs) ->
|
start(_StartType, _StartArgs) ->
|
||||||
|
case infra_secrets:validate_startup() of
|
||||||
|
ok ->
|
||||||
|
start_application();
|
||||||
|
{error, Reason} ->
|
||||||
|
io:format("~nFATAL: secret validation failed: ~p~n", [Reason]),
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
start_application() ->
|
||||||
case infra_sup:start_link() of
|
case infra_sup:start_link() of
|
||||||
{ok, Pid} ->
|
{ok, Pid} ->
|
||||||
% Определяем список узлов кластера, если режим CLUSTER_MODE=true
|
% Определяем список узлов кластера, если режим CLUSTER_MODE=true
|
||||||
@@ -48,8 +57,10 @@ start(_StartType, _StartArgs) ->
|
|||||||
start_swagger_http(), % Swagger UI и спецификация (8447)
|
start_swagger_http(), % Swagger UI и спецификация (8447)
|
||||||
application:ensure_all_started(prometheus),
|
application:ensure_all_started(prometheus),
|
||||||
application:ensure_all_started(prometheus_cowboy),
|
application:ensure_all_started(prometheus_cowboy),
|
||||||
init_default_admins(),
|
case init_default_admins() of
|
||||||
{ok, Pid};
|
ok -> {ok, Pid};
|
||||||
|
{error, Reason} -> {error, Reason}
|
||||||
|
end;
|
||||||
Error ->
|
Error ->
|
||||||
Error
|
Error
|
||||||
end.
|
end.
|
||||||
@@ -199,31 +210,49 @@ start_swagger_http() ->
|
|||||||
init_default_admins() ->
|
init_default_admins() ->
|
||||||
case core_admin:list_all() of
|
case core_admin:list_all() of
|
||||||
[] ->
|
[] ->
|
||||||
% Суперадмин
|
seed_default_admins();
|
||||||
SuperEmail = list_to_binary(os:getenv("ADMIN_SUPER_EMAIL", "superadmin@eventhub.local")),
|
|
||||||
SuperPass = list_to_binary(os:getenv("ADMIN_SUPER_PASSWORD", "123456")),
|
|
||||||
{ok, _} = core_admin:create(SuperEmail, SuperPass, superadmin),
|
|
||||||
io:format("Default superadmin created: ~s~n", [SuperEmail]),
|
|
||||||
|
|
||||||
% Админ
|
|
||||||
AdminEmail = list_to_binary(os:getenv("ADMIN_EMAIL", "admin@eventhub.local")),
|
|
||||||
AdminPass = list_to_binary(os:getenv("ADMIN_PASSWORD", "123456")),
|
|
||||||
{ok, _} = core_admin:create(AdminEmail, AdminPass, admin),
|
|
||||||
io:format("Default admin created: ~s~n", [AdminEmail]),
|
|
||||||
|
|
||||||
% Модератор
|
|
||||||
ModerEmail = list_to_binary(os:getenv("ADMIN_MODER_EMAIL", "moderator@eventhub.local")),
|
|
||||||
ModerPass = list_to_binary(os:getenv("ADMIN_MODER_PASSWORD", "123456")),
|
|
||||||
{ok, _} = core_admin:create(ModerEmail, ModerPass, moderator),
|
|
||||||
io:format("Default moderator created: ~s~n", [ModerEmail]),
|
|
||||||
|
|
||||||
% Поддержка
|
|
||||||
SupportEmail = list_to_binary(os:getenv("ADMIN_SUPPORT_EMAIL", "support@eventhub.local")),
|
|
||||||
SupportPass = list_to_binary(os:getenv("ADMIN_SUPPORT_PASSWORD", "123456")),
|
|
||||||
{ok, _} = core_admin:create(SupportEmail, SupportPass, support),
|
|
||||||
io:format("Default support created: ~s~n", [SupportEmail]);
|
|
||||||
_ ->
|
_ ->
|
||||||
io:format("Admins already exist. Skipping creation.~n")
|
io:format("Admins already exist. Skipping creation.~n"),
|
||||||
|
ok
|
||||||
|
end.
|
||||||
|
|
||||||
|
seed_default_admins() ->
|
||||||
|
Admins = [
|
||||||
|
{superadmin, "ADMIN_SUPER_EMAIL", "superadmin@eventhub.local",
|
||||||
|
"ADMIN_SUPER_PASSWORD", "123456"},
|
||||||
|
{admin, "ADMIN_EMAIL", "admin@eventhub.local",
|
||||||
|
"ADMIN_PASSWORD", "123456"},
|
||||||
|
{moderator, "ADMIN_MODER_EMAIL", "moderator@eventhub.local",
|
||||||
|
"ADMIN_MODER_PASSWORD", "123456"},
|
||||||
|
{support, "ADMIN_SUPPORT_EMAIL", "support@eventhub.local",
|
||||||
|
"ADMIN_SUPPORT_PASSWORD", "123456"}
|
||||||
|
],
|
||||||
|
lists:foldl(fun seed_admin/2, ok, Admins).
|
||||||
|
|
||||||
|
seed_admin({Role, EmailKey, DefaultEmail, PassKey, DefaultPass}, ok) ->
|
||||||
|
case admin_credentials(EmailKey, DefaultEmail, PassKey, DefaultPass) of
|
||||||
|
{error, _} = Err -> Err;
|
||||||
|
{ok, {Email, Pass}} ->
|
||||||
|
{ok, _} = core_admin:create(Email, Pass, Role),
|
||||||
|
io:format("Default ~p created: ~s~n", [Role, Email]),
|
||||||
|
ok
|
||||||
|
end;
|
||||||
|
seed_admin(_, Err) -> Err.
|
||||||
|
|
||||||
|
admin_credentials(EmailKey, DefaultEmail, PassKey, DefaultPass) ->
|
||||||
|
case infra_secrets:is_dev() of
|
||||||
|
true ->
|
||||||
|
{ok, {list_to_binary(os:getenv(EmailKey, DefaultEmail)),
|
||||||
|
list_to_binary(os:getenv(PassKey, DefaultPass))}};
|
||||||
|
false ->
|
||||||
|
case infra_secrets:require_admin_seed_email(EmailKey) of
|
||||||
|
{error, _} = Err -> Err;
|
||||||
|
{ok, Email} ->
|
||||||
|
case infra_secrets:require_admin_seed_password(PassKey) of
|
||||||
|
{error, _} = Err -> Err;
|
||||||
|
{ok, Pass} -> {ok, {Email, Pass}}
|
||||||
|
end
|
||||||
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
get_env_int(Key, Default) ->
|
get_env_int(Key, Default) ->
|
||||||
|
|||||||
@@ -24,7 +24,11 @@ get_user_secret() ->
|
|||||||
|
|
||||||
get_user_secret_from_env() ->
|
get_user_secret_from_env() ->
|
||||||
case os:getenv("JWT_SECRET") of
|
case os:getenv("JWT_SECRET") of
|
||||||
false -> <<"user-secret-key-32-bytes-minimum!">>;
|
false ->
|
||||||
|
case infra_secrets:is_dev() of
|
||||||
|
true -> <<"user-secret-key-32-bytes-minimum!">>;
|
||||||
|
false -> error({missing_secret, jwt_secret})
|
||||||
|
end;
|
||||||
S -> list_to_binary(S)
|
S -> list_to_binary(S)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
@@ -37,7 +41,11 @@ get_admin_secret() ->
|
|||||||
|
|
||||||
get_admin_secret_from_env() ->
|
get_admin_secret_from_env() ->
|
||||||
case os:getenv("ADMIN_JWT_SECRET") of
|
case os:getenv("ADMIN_JWT_SECRET") of
|
||||||
false -> <<"admin-secret-key-32-bytes-minimum!">>;
|
false ->
|
||||||
|
case infra_secrets:is_dev() of
|
||||||
|
true -> <<"admin-secret-key-32-bytes-minimum!">>;
|
||||||
|
false -> error({missing_secret, admin_jwt_secret})
|
||||||
|
end;
|
||||||
S -> list_to_binary(S)
|
S -> list_to_binary(S)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Валидация секретов и паролей при старте (EVENTHUB_ENV).
|
||||||
|
%%% dev — допускаются documented fallbacks; stage/prod — fail-fast.
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(infra_secrets).
|
||||||
|
-export([mode/0, is_dev/0, validate_startup/0,
|
||||||
|
require_admin_seed_password/1, require_admin_seed_email/1]).
|
||||||
|
|
||||||
|
-define(MIN_SECRET_LEN, 32).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% API
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec mode() -> dev | stage | prod.
|
||||||
|
mode() ->
|
||||||
|
case os:getenv("EVENTHUB_ENV") of
|
||||||
|
false -> dev;
|
||||||
|
Env ->
|
||||||
|
case string:lowercase(Env) of
|
||||||
|
"dev" -> dev;
|
||||||
|
"stage" -> stage;
|
||||||
|
"prod" -> prod;
|
||||||
|
"production" -> prod;
|
||||||
|
_ -> stage
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec is_dev() -> boolean().
|
||||||
|
is_dev() -> mode() =:= dev.
|
||||||
|
|
||||||
|
%% @doc Проверка JWT-секретов до поднятия Mnesia/HTTP.
|
||||||
|
-spec validate_startup() -> ok | {error, {missing_secret, atom()} | {weak_secret, atom()}}.
|
||||||
|
validate_startup() ->
|
||||||
|
case mode() of
|
||||||
|
dev ->
|
||||||
|
dev_warn_if_weak(jwt_secret, effective_jwt_secret()),
|
||||||
|
dev_warn_if_weak(admin_jwt_secret, effective_admin_jwt_secret()),
|
||||||
|
ok;
|
||||||
|
_ ->
|
||||||
|
case require_strong_secret(jwt_secret, effective_jwt_secret()) of
|
||||||
|
ok -> require_strong_secret(admin_jwt_secret, effective_admin_jwt_secret());
|
||||||
|
Err -> Err
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec require_admin_seed_email(string()) ->
|
||||||
|
{ok, binary()} | {error, {missing_secret, atom()}}.
|
||||||
|
require_admin_seed_email(EnvKey) ->
|
||||||
|
case os:getenv(EnvKey) of
|
||||||
|
false -> {error, {missing_secret, list_to_atom(EnvKey)}};
|
||||||
|
"" -> {error, {missing_secret, list_to_atom(EnvKey)}};
|
||||||
|
Email -> {ok, list_to_binary(Email)}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec require_admin_seed_password(string()) ->
|
||||||
|
{ok, binary()} | {error, {missing_secret, atom()} | {weak_secret, atom()}}.
|
||||||
|
require_admin_seed_password(EnvKey) ->
|
||||||
|
case os:getenv(EnvKey) of
|
||||||
|
false -> {error, {missing_secret, list_to_atom(EnvKey)}};
|
||||||
|
"" -> {error, {missing_secret, list_to_atom(EnvKey)}};
|
||||||
|
Pass ->
|
||||||
|
Bin = list_to_binary(Pass),
|
||||||
|
case is_strong_secret(Bin) of
|
||||||
|
true -> {ok, Bin};
|
||||||
|
false -> {error, {weak_secret, list_to_atom(EnvKey)}}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% Effective values (порядок как в eventhub_auth)
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
|
||||||
|
effective_jwt_secret() ->
|
||||||
|
effective_secret(jwt_secret, "JWT_SECRET").
|
||||||
|
|
||||||
|
effective_admin_jwt_secret() ->
|
||||||
|
effective_secret(admin_jwt_secret, "ADMIN_JWT_SECRET").
|
||||||
|
|
||||||
|
effective_secret(AppKey, OsKey) ->
|
||||||
|
case application:get_env(eventhub, AppKey) of
|
||||||
|
{ok, Secret} when is_binary(Secret), byte_size(Secret) > 0 ->
|
||||||
|
case is_placeholder(Secret) of
|
||||||
|
true -> os_getenv_binary(OsKey);
|
||||||
|
false -> Secret
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
os_getenv_binary(OsKey)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% Internal
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
|
||||||
|
require_strong_secret(Name, undefined) ->
|
||||||
|
{error, {missing_secret, Name}};
|
||||||
|
require_strong_secret(Name, Secret) ->
|
||||||
|
case is_strong_secret(Secret) of
|
||||||
|
true -> ok;
|
||||||
|
false -> {error, {weak_secret, Name}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
is_strong_secret(Secret) when is_binary(Secret) ->
|
||||||
|
byte_size(Secret) >= ?MIN_SECRET_LEN andalso not lists:member(Secret, denylist()).
|
||||||
|
|
||||||
|
is_placeholder(Secret) ->
|
||||||
|
lists:member(Secret, [
|
||||||
|
<<"change_me_in_production">>,
|
||||||
|
<<"${JWT_SECRET:-change_me_in_production}">>,
|
||||||
|
<<"${ADMIN_JWT_SECRET:-change_me_in_production}">>
|
||||||
|
]).
|
||||||
|
|
||||||
|
denylist() -> [
|
||||||
|
<<"123456">>,
|
||||||
|
<<"change_me_in_production">>,
|
||||||
|
<<"eventhub_top_secret">>,
|
||||||
|
<<"user-secret-key-32-bytes-minimum!">>,
|
||||||
|
<<"admin-secret-key-32-bytes-minimum!">>,
|
||||||
|
<<"botpass123">>
|
||||||
|
].
|
||||||
|
|
||||||
|
os_getenv_binary(Key) ->
|
||||||
|
case os:getenv(Key) of
|
||||||
|
false -> undefined;
|
||||||
|
"" -> undefined;
|
||||||
|
Val -> list_to_binary(Val)
|
||||||
|
end.
|
||||||
|
|
||||||
|
dev_warn_if_weak(_Name, undefined) -> ok;
|
||||||
|
dev_warn_if_weak(Name, Secret) ->
|
||||||
|
case is_strong_secret(Secret) of
|
||||||
|
true -> ok;
|
||||||
|
false ->
|
||||||
|
io:format("~n[infra_secrets] WARNING: ~p is weak or missing in dev mode; "
|
||||||
|
"runtime fallbacks may apply.~n", [Name])
|
||||||
|
end.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
-module(infra_secrets_tests).
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
-define(STRONG_JWT, <<"this-is-a-strong-user-jwt-secret-32b">>).
|
||||||
|
-define(STRONG_ADMIN, <<"this-is-a-strong-admin-jwt-secret32">>).
|
||||||
|
|
||||||
|
setup() ->
|
||||||
|
application:load(eventhub),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
cleanup(_) ->
|
||||||
|
application:unset_env(eventhub, jwt_secret),
|
||||||
|
application:unset_env(eventhub, admin_jwt_secret),
|
||||||
|
os:unsetenv("EVENTHUB_ENV"),
|
||||||
|
os:unsetenv("JWT_SECRET"),
|
||||||
|
os:unsetenv("ADMIN_JWT_SECRET"),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
with_env(Key, Val, Fun) ->
|
||||||
|
os:putenv(Key, Val),
|
||||||
|
try Fun() after os:unsetenv(Key) end.
|
||||||
|
|
||||||
|
infra_secrets_test_() ->
|
||||||
|
{foreach, fun setup/0, fun cleanup/1, [
|
||||||
|
{"dev mode allows missing secrets", fun test_dev_missing_ok/0},
|
||||||
|
{"stage rejects missing JWT_SECRET", fun test_stage_missing_jwt/0},
|
||||||
|
{"stage rejects weak JWT_SECRET", fun test_stage_weak_jwt/0},
|
||||||
|
{"stage accepts strong secrets", fun test_stage_strong_ok/0},
|
||||||
|
{"require_admin_seed_password rejects 123456", fun test_weak_admin_password/0}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
test_dev_missing_ok() ->
|
||||||
|
with_env("EVENTHUB_ENV", "dev", fun() ->
|
||||||
|
?assertEqual(dev, infra_secrets:mode()),
|
||||||
|
?assertEqual(ok, infra_secrets:validate_startup())
|
||||||
|
end).
|
||||||
|
|
||||||
|
test_stage_missing_jwt() ->
|
||||||
|
with_env("EVENTHUB_ENV", "stage", fun() ->
|
||||||
|
?assertMatch({error, {missing_secret, jwt_secret}},
|
||||||
|
infra_secrets:validate_startup())
|
||||||
|
end).
|
||||||
|
|
||||||
|
test_stage_weak_jwt() ->
|
||||||
|
with_env("EVENTHUB_ENV", "stage", fun() ->
|
||||||
|
os:putenv("JWT_SECRET", "123456"),
|
||||||
|
os:putenv("ADMIN_JWT_SECRET", binary_to_list(?STRONG_ADMIN)),
|
||||||
|
?assertMatch({error, {weak_secret, jwt_secret}},
|
||||||
|
infra_secrets:validate_startup())
|
||||||
|
end).
|
||||||
|
|
||||||
|
test_stage_strong_ok() ->
|
||||||
|
with_env("EVENTHUB_ENV", "stage", fun() ->
|
||||||
|
os:putenv("JWT_SECRET", binary_to_list(?STRONG_JWT)),
|
||||||
|
os:putenv("ADMIN_JWT_SECRET", binary_to_list(?STRONG_ADMIN)),
|
||||||
|
?assertEqual(ok, infra_secrets:validate_startup())
|
||||||
|
end).
|
||||||
|
|
||||||
|
test_weak_admin_password() ->
|
||||||
|
os:putenv("ADMIN_PASSWORD", "123456"),
|
||||||
|
?assertMatch({error, {weak_secret, 'ADMIN_PASSWORD'}},
|
||||||
|
infra_secrets:require_admin_seed_password("ADMIN_PASSWORD")).
|
||||||
Reference in New Issue
Block a user