353 lines
20 KiB
Makefile
353 lines
20 KiB
Makefile
# «Контракт-чек» — everyday commands (uv + docker)
|
|
# Usage: make <target> (see `make help`)
|
|
|
|
.PHONY: help install lint lint-fix isort isort-check typecheck test test-unit test-integration migrate \
|
|
infra-up infra-down infra-logs services-up services-down services-logs services-ps \
|
|
full-up full-down full-logs full-ps \
|
|
services-obs services-observer services-nginx \
|
|
obs-up obs-down obs-logs obs-url obs-reset \
|
|
observer-up observer-down observer-logs \
|
|
nginx-up nginx-down nginx-logs nginx-ps \
|
|
api api-logs bot bot-up bot-down bot-logs bot-ps worker-extract worker-analyze worker-notify \
|
|
bot-remote-up bot-remote-down bot-remote-logs bot-remote-ps bot-webhook-path \
|
|
seed-token jwt-secret jwt-token jwt-verify health shell-api shell-bot \
|
|
shell-db admin-promote admin-list \
|
|
nomad-validate nomad-plan nomad-deploy nomad-status nomad-logs nomad-scale nomad-revert \
|
|
clean dev dev-obs stop stop-obs
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Help
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
help: ## Show available commands
|
|
@echo "Usage: make <target>"
|
|
@grep -E '^[a-zA-Z0-9_-]+:.*##' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*##"}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Local dev (uv)
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
install: ## Sync dev dependencies (uv)
|
|
uv sync --group dev
|
|
|
|
lint: ## Run ruff linter + format check
|
|
uv run ruff check src tests
|
|
uv run ruff format --check src tests
|
|
|
|
lint-fix: ## Run ruff autofix + format + isort fix
|
|
uv run ruff check --fix src tests
|
|
uv run ruff format src tests
|
|
uv run isort src tests
|
|
|
|
isort: ## Run isort (sort imports in-place)
|
|
uv run isort src tests
|
|
|
|
isort-check: ## Run isort in check-only mode
|
|
uv run isort --check-only src tests
|
|
|
|
typecheck: ## Run ty type checker
|
|
uv run ty check src
|
|
|
|
test: ## Run all tests (unit + integration)
|
|
uv run pytest
|
|
|
|
test-unit: ## Run unit tests only (fast)
|
|
uv run pytest -m "not integration"
|
|
|
|
test-cov: ## Run unit tests with coverage report (enforces threshold)
|
|
uv run pytest --cov=src/contract_check --cov-branch --cov-report=term-missing --cov-report=html --cov-fail-under=59 -m "not integration" tests/unit
|
|
|
|
test-integration: ## Run integration tests (needs docker infra)
|
|
uv run pytest -m integration
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Docker: infrastructure (postgres + redis + rabbitmq + minio)
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
infra-up: ## Start infra containers
|
|
docker compose up -d
|
|
|
|
infra-down: ## Stop infra containers
|
|
docker compose down
|
|
|
|
infra-logs: ## Tail infra logs
|
|
docker compose logs -f
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Docker: api + workers (no bot)
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
services-up: ## Start api + worker services (needs infra running; no bot)
|
|
docker compose --profile services up -d --build --remove-orphans
|
|
|
|
services-down: ## Stop api + worker services
|
|
docker compose --profile services down
|
|
|
|
services-logs: ## Tail api + worker logs
|
|
docker compose --profile services logs -f
|
|
|
|
services-ps: ## Show api + worker containers
|
|
docker compose --profile services ps
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Docker: full stack (api + workers + bot on the same host)
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
full-up: ## Start full stack: api + workers + bot (needs infra running)
|
|
docker compose --profile services --profile bot up -d --build --remove-orphans
|
|
|
|
full-down: ## Stop full stack: api + workers + bot
|
|
docker compose --profile services --profile bot down
|
|
|
|
full-logs: ## Tail full stack logs (api + workers + bot)
|
|
docker compose --profile services --profile bot logs -f
|
|
|
|
full-ps: ## Show full stack containers (api + workers + bot)
|
|
docker compose --profile services --profile bot ps
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Docker: combined with observability / edge
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
services-obs: ## Start api + workers + bot + Grafana/Loki/Prometheus
|
|
docker compose --profile services --profile bot --profile obs up -d --build --remove-orphans
|
|
|
|
services-observer: ## Start api + workers + bot + OpenObserve collector stack
|
|
docker compose --profile services --profile bot --profile observer up -d --build --remove-orphans
|
|
|
|
services-nginx: ## Start full stack + nginx reverse proxy
|
|
docker compose --profile services --profile bot --profile edge up -d --build --remove-orphans
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Docker: observability (Grafana + Loki logs)
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
obs-up: ## Start Grafana/Loki/Prometheus containers
|
|
docker compose --profile obs up -d --build --remove-orphans
|
|
|
|
obs-down: ## Stop Grafana/Loki/Prometheus containers
|
|
docker compose --profile obs down
|
|
|
|
obs-logs: ## Tail Grafana/Loki/Prometheus containers logs
|
|
docker compose --profile obs logs -f
|
|
|
|
obs-url: ## Print Grafana URL and default credentials
|
|
@echo "Direct: http://localhost:$${GRAFANA_PORT:-3000}"
|
|
@echo "Via nginx: $${GRAFANA_ROOT_URL:-http://localhost}/grafana/"
|
|
@echo "User: $${GRAFANA_ADMIN_USER:-admin}"
|
|
@echo "Password: $${GRAFANA_ADMIN_PASSWORD:-admin}"
|
|
@echo "Dashboard: $${GRAFANA_ROOT_URL:-http://localhost}/grafana/d/contract-check-logs"
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Docker: OpenObserve collector stack
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
observer-up: ## Start OpenObserve + Vector containers
|
|
docker compose --profile observer up -d --build --remove-orphans
|
|
|
|
observer-down: ## Stop OpenObserve + Vector containers
|
|
docker compose --profile observer down
|
|
|
|
observer-logs: ## Tail OpenObserve + Vector containers logs
|
|
docker compose --profile observer logs -f
|
|
|
|
obs-reset: ## Reset Grafana and Loki volumes (wipes dashboards/logs data)
|
|
docker compose --profile obs down -v
|
|
rm -rf deploy/observability/grafana/dashboards/*.json.tmp
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Docker: nginx edge (reverse proxy)
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
nginx-up: ## Start nginx
|
|
docker compose --profile edge up -d --build --remove-orphans
|
|
|
|
nginx-down: ## Stop nginx
|
|
docker compose --profile edge down
|
|
|
|
nginx-logs: ## Tail nginx logs
|
|
docker compose --profile edge logs -f
|
|
|
|
nginx-ps: ## Show running containers
|
|
docker compose --profile edge ps
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Individual services
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
api: ## Start/restart API service
|
|
docker compose --profile services up -d --build --remove-orphans api
|
|
|
|
api-logs: ## Tail API logs
|
|
docker compose logs -f api
|
|
|
|
bot: bot-up ## Alias for bot-up
|
|
|
|
bot-up: ## Start/restart Telegram bot (same host as the api stack)
|
|
docker compose --profile bot up -d --build --remove-orphans bot
|
|
|
|
bot-down: ## Stop Telegram bot
|
|
docker compose --profile bot down
|
|
|
|
bot-logs: ## Tail bot logs (same host as the api stack)
|
|
docker compose logs -f bot
|
|
|
|
bot-ps: ## Show bot container status
|
|
docker compose --profile bot ps
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Docker: bot on a separate server (HTTP-only adapter to central API)
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
bot-remote-up: ## Start bot on a separate server (set API_URL to central API)
|
|
docker compose -f docker-compose.bot.yml up -d --build --remove-orphans
|
|
|
|
bot-remote-down: ## Stop remote bot
|
|
docker compose -f docker-compose.bot.yml down
|
|
|
|
bot-remote-logs: ## Tail remote bot logs
|
|
docker compose -f docker-compose.bot.yml logs -f
|
|
|
|
bot-remote-ps: ## Show remote bot container status
|
|
docker compose -f docker-compose.bot.yml ps
|
|
|
|
bot-webhook-path: ## Print the secret-derived Telegram webhook path (reads BOT_TOKEN + BOT_WEBHOOK_SECRET_TOKEN from .env)
|
|
@uv run python -c "import os; from dotenv import load_dotenv; load_dotenv(); \
|
|
from src.contract_check.bot.webhook import derive_webhook_path; \
|
|
print(derive_webhook_path(os.environ['BOT_TOKEN'], os.environ['BOT_WEBHOOK_SECRET_TOKEN']))"
|
|
|
|
worker-extract: ## Start/restart extract worker
|
|
docker compose --profile services up -d --build --remove-orphans worker-extract
|
|
|
|
worker-analyze: ## Start/restart analyze worker
|
|
docker compose --profile services up -d --build --remove-orphans worker-analyze
|
|
|
|
worker-analyze-logs: ## Tail analyze worker logs
|
|
docker compose logs -f worker-analyze
|
|
|
|
worker-prescreen: ## Start/restart prescreen worker
|
|
docker compose --profile services up -d --build --remove-orphans worker-prescreen
|
|
|
|
worker-prescreen-logs: ## Tail prescreen worker logs
|
|
docker compose logs -f worker-prescreen
|
|
|
|
worker-notify: ## Start/restart notify worker
|
|
docker compose --profile services up -d --build --remove-orphans worker-notify
|
|
|
|
worker-notify-logs: ## Tail notify worker logs
|
|
docker compose logs -f worker-notify
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Database
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
migrate: ## Run Alembic migrations (inside api container)
|
|
docker compose --profile services build api
|
|
docker compose --profile services run --rm api alembic upgrade head
|
|
|
|
shell-db: ## Open psql inside postgres container
|
|
docker compose exec postgres psql -U contract_check -d contract_check
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Admin panel (/admin — manage users; needs role = 'admin')
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
admin-promote: ## Grant admin role to a user (usage: make admin-promote EMAIL=a@b.c)
|
|
@if [ -z "$(EMAIL)" ]; then \
|
|
echo "Usage: make admin-promote EMAIL=a@b.c"; \
|
|
exit 1; \
|
|
fi
|
|
@docker compose exec -T postgres psql -U contract_check -d contract_check \
|
|
-c "UPDATE users SET role = 'admin' WHERE email = '$(EMAIL)';" \
|
|
-c "SELECT id, email, role FROM users WHERE email = '$(EMAIL)';"
|
|
|
|
admin-list: ## List current admin users
|
|
@docker compose exec -T postgres psql -U contract_check -d contract_check \
|
|
-c "SELECT id, email, telegram_id, role, is_active FROM users WHERE role = 'admin';"
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Auth / tokens
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
seed-token: ## Generate bot service token (prints bearer token)
|
|
docker compose --profile services exec api python -m src.contract_check.api seed-token bot-prod bot
|
|
|
|
jwt-secret: ## Generate a fresh JWT_SECRET for .env
|
|
@openssl rand -hex 32
|
|
|
|
jwt-token: ## Exchange a telegram_id for a user JWT (usage: make jwt-token TG_ID=123456)
|
|
@if [ -z "$(TG_ID)" ]; then \
|
|
echo "Usage: make jwt-token TG_ID=123456"; \
|
|
exit 1; \
|
|
fi
|
|
@TOKEN=$$(grep -E '^BOT_SERVICE_TOKEN=' .env | cut -d= -f2); \
|
|
if [ -z "$$TOKEN" ]; then \
|
|
echo "BOT_SERVICE_TOKEN not found in .env"; \
|
|
exit 1; \
|
|
fi; \
|
|
curl -s -X POST -H "Authorization: Bearer $$TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"telegram_id":$(TG_ID)}' \
|
|
http://localhost:8000/api/v1/auth/telegram/bot | jq .
|
|
|
|
jwt-verify: ## Introspect a user JWT (usage: make jwt-verify JWT=eyJ...)
|
|
@if [ -z "$(JWT)" ]; then \
|
|
echo "Usage: make jwt-verify JWT=eyJ..."; \
|
|
exit 1; \
|
|
fi
|
|
@curl -s -H "Authorization: Bearer $(JWT)" \
|
|
http://localhost:8000/api/v1/auth/me | jq .
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Health / diagnostics
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
health: ## Check API health endpoint
|
|
@echo "API health:"
|
|
@curl -s http://localhost:8000/healthz | jq . 2>/dev/null || curl -s http://localhost:8000/healthz
|
|
@echo ""
|
|
@echo "RabbitMQ: http://localhost:15672 (guest/guest → contract_check/contract_check)"
|
|
@echo "MinIO: http://localhost:9001 (contract_check/contract_check)"
|
|
@echo "Postgres: localhost:15432 (contract_check/contract_check)"
|
|
|
|
shell-api: ## Open shell inside API container
|
|
docker compose --profile services exec api /bin/sh
|
|
|
|
shell-bot: ## Open shell inside bot container
|
|
docker compose --profile bot exec bot /bin/sh
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Nomad (production app services; full runbook: deploy/nomad/README.md)
|
|
# CLI env (NOMAD_ADDR/CACERT/TOKEN) is sourced from /root/.nomadrc on the VPS.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
NOMAD_JOB := deploy/nomad/contract-check.nomad.hcl
|
|
NOMAD_RUN := bash -c '. /root/.nomadrc 2>/dev/null; IMAGE_TAG=$${IMAGE_TAG:-manual} envsubst < $(NOMAD_JOB) | nomad "$$@"' --
|
|
|
|
nomad-validate: ## Validate the Nomad job file
|
|
@$(NOMAD_RUN) job validate -
|
|
|
|
nomad-plan: ## Dry-run diff of the next deployment
|
|
@$(NOMAD_RUN) job plan -
|
|
|
|
nomad-deploy: ## Deploy to Nomad (usage: IMAGE_TAG=<git-sha> make nomad-deploy)
|
|
@$(NOMAD_RUN) job run -
|
|
|
|
nomad-status: ## Job status: groups, allocations, deployments
|
|
@$(NOMAD_RUN) job status contract-check
|
|
|
|
nomad-logs: ## Tail a group's logs (usage: make nomad-logs G=api)
|
|
@alloc=$$($(NOMAD_RUN) job allocs -json contract-check | python3 -c \
|
|
"import json,sys; a=[x for x in json.load(sys.stdin) if x['TaskGroup']=='$(G)' and x['ClientStatus']=='running']; print(a[0]['ID'] if a else '')"); \
|
|
[ -n "$$alloc" ] && $(NOMAD_RUN) alloc logs -f $$alloc || echo "no running alloc in group $(G)"
|
|
|
|
nomad-scale: ## Scale a group (usage: make nomad-scale G=worker-extract N=3)
|
|
@$(NOMAD_RUN) job scale contract-check $(G) $(N)
|
|
|
|
nomad-revert: ## Revert job to a prior version (usage: make nomad-revert V=2)
|
|
@$(NOMAD_RUN) job revert contract-check $(V)
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Cleanup
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
clean: ## Remove containers, volumes, caches
|
|
docker compose --profile services down -v
|
|
docker compose --profile bot down -v
|
|
docker compose down -v
|
|
rm -rf .pytest_cache .ruff_cache
|
|
uv cache clean
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Full workflow shortcuts
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
dev: install infra-up migrate services-up bot-up ## Bootstrap full dev environment (with bot)
|
|
|
|
dev-obs: install infra-up migrate services-obs ## Bootstrap full dev environment with observability
|
|
|
|
stop: services-down bot-down infra-down ## Stop everything
|
|
|
|
stop-obs: obs-down services-down bot-down infra-down ## Stop everything including observability
|