Add DAG and migration_reporter module
This commit is contained in:
@@ -0,0 +1,117 @@
|
|||||||
|
# Airflow: настройка пайплайна отчетов по миграциям
|
||||||
|
|
||||||
|
DAG: `dags/migration_reporter_dag.py` — расписание **Пн / Ср / Пт 08:00** (`0 8 * * 1,3,5`).
|
||||||
|
|
||||||
|
Граф задач:
|
||||||
|
|
||||||
|
```
|
||||||
|
pull_repo → generate_excel → generate_html
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт пайплайна: `migration_reporter.py` (лежит в корне репозитория миграций).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Установка Airflow 3 (docker-compose)
|
||||||
|
|
||||||
|
В каталоге проекта Airflow (например, `/projects/airflow-docker`) — по официальной документации
|
||||||
|
https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html
|
||||||
|
|
||||||
|
Файл `.env` там же:
|
||||||
|
|
||||||
|
```env
|
||||||
|
_AIP_UID=..
|
||||||
|
# дополнительные пакеты в контейнерах (быстрый способ для теста)
|
||||||
|
_PIP_ADDITIONAL_REQUIREMENTS=openpyxl
|
||||||
|
```
|
||||||
|
|
||||||
|
> Для постоянной работы лучше свой Dockerfile:
|
||||||
|
> ```dockerfile
|
||||||
|
> FROM apache/airflow:3.3.2
|
||||||
|
> RUN pip install openpyxl
|
||||||
|
> ```
|
||||||
|
> и в `docker-compose.yaml` в `image:` указать имя собранного образа
|
||||||
|
> (`docker build -t airflow-with-pydeps .`).
|
||||||
|
|
||||||
|
## 2. Volume-монтирования (репозиторий + папка отчетов)
|
||||||
|
|
||||||
|
В `docker-compose.yaml` в общий блок `x-airflow-common`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
x-airflow-common: &airflow-common
|
||||||
|
volumes:
|
||||||
|
- ./dags:/opt/airflow/dags
|
||||||
|
- ./config:/opt/airflow/config
|
||||||
|
- ./logs:/opt/airflow/logs
|
||||||
|
- ./plugins:/opt/airflow/plugins
|
||||||
|
# путь на хосте (Windows) : путь в контейнере
|
||||||
|
- C:/Users/veremeytsev_dg/Desktop/migrations_tool:/opt/airflow/migrations_tool # репозиторий миграций
|
||||||
|
- C:/Users/veremeytsev_dg/Desktop/migrations_reports:/opt/airflow/reports # общая папка отчетов
|
||||||
|
```
|
||||||
|
|
||||||
|
После этого перезапустить: `docker compose up -d`.
|
||||||
|
|
||||||
|
Дагфайл кладется в `./dags/migration_reporter_dag.py` (папка dags уже смонтирована).
|
||||||
|
|
||||||
|
## 3. Git-доступ из контейнера
|
||||||
|
|
||||||
|
На хосте Windows аутентификация работает через credential-manager — в контейнере его нет.
|
||||||
|
Два варианта (один раз, внутри контейнера):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# а) Токен в remote-url (быстро; хранить токен в Airflow Variable и клонировать им)
|
||||||
|
docker compose exec airflow-worker git clone \
|
||||||
|
https://<user>:<token>@seedvault.coderocket.ru/SZA_analytics/migrations.git \
|
||||||
|
/opt/airflow/migrations_tool
|
||||||
|
|
||||||
|
# б) SSH-deploy key в volume (надежнее для продакшена)
|
||||||
|
# смонтировать ключ и: git -c core.sshCommand="ssh -i /opt/airflow/ssh/key" clone ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверка: `docker compose exec airflow-worker git -C /opt/airflow/migrations_tool pull --ff-only`
|
||||||
|
|
||||||
|
## 4. Airflow Variables (пути)
|
||||||
|
|
||||||
|
В UI: Admin → Variables, либо CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec airflow-scheduler airflow variables set migrations_repo_dir /opt/airflow/migrations_tool
|
||||||
|
docker compose exec airflow-scheduler airflow variables set migrations_reports_dir /opt/airflow/reports
|
||||||
|
```
|
||||||
|
|
||||||
|
Можно не задавать — в DAG есть env-fallback (`MIGRATIONS_REPO_DIR`, `MIGRATIONS_REPORTS_DIR`).
|
||||||
|
|
||||||
|
## 5. Запуск и проверка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec airflow-scheduler airflow dags list
|
||||||
|
docker compose exec airflow-scheduler airflow dags trigger migration_reporter_pipeline
|
||||||
|
docker compose exec airflow-scheduler airflow tasks states-for-run migration_reporter_pipeline <run_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
Артефакты прогона (в общей папке `/opt/airflow/reports` ↔ хост):
|
||||||
|
|
||||||
|
```
|
||||||
|
migration_report_YYYYMMDD_HHMMSS.xlsx # Excel (11 листов)
|
||||||
|
migration_report_YYYYMMDD_HHMMSS.changes.json # кэш изменений (между задачами DAG)
|
||||||
|
migration_report_YYYYMMDD_HHMMSS.html # HTML-отчет (батчи по дате загрузки)
|
||||||
|
latest.xlsx / latest.html # фиксированные имена последнего отчета
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Локальный запуск без Airflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd migrations_tool
|
||||||
|
python migration_reporter.py run --dir pgsql --skip-pull # без git pull
|
||||||
|
python migration_reporter.py run --dir pgsql # с git pull
|
||||||
|
python migration_reporter.py excel --dir pgsql --output r.xlsx # только Excel
|
||||||
|
python migration_reporter.py html --cache r.changes.json --output r.html # только HTML
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Ручная генерация отдельного шага
|
||||||
|
|
||||||
|
| Команда | Что делает |
|
||||||
|
|---|---|
|
||||||
|
| `excel` | анализ + Excel-отчет + `.changes.json` кэш |
|
||||||
|
| `html` | HTML из кэша (быстро, без повторного анализа) |
|
||||||
|
| `run` | полный цикл (для локального запуска / восстановления) |
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,113 @@
|
|||||||
|
"""
|
||||||
|
DAG: пайплайн отчетов по миграциям.
|
||||||
|
|
||||||
|
Этапы (каждая задача видна отдельно в Airflow UI):
|
||||||
|
1. pull_repo — git pull локальной копии репозитория миграций
|
||||||
|
2. generate_excel — анализ .kt файлов + Excel-отчет + кэш изменений (JSON)
|
||||||
|
3. generate_html — HTML-отчет из кэша (батчи по дате загрузки + фильтры)
|
||||||
|
|
||||||
|
Расписание: Пн / Ср / Пт в 08:00 (cron "0 8 * * 1,3,5").
|
||||||
|
|
||||||
|
Требования к окружению:
|
||||||
|
- пакет openpyxl в образе (_PIP_ADDITIONAL_REQUIREMENTS=openpyxl или кастомный Dockerfile);
|
||||||
|
- git в контейнере + доступ до origin (token в remote-url или ssh-key, см. AIRFLOW_SETUP.md);
|
||||||
|
- volume-монтирования: репозиторий миграций и папка отчетов (общая).
|
||||||
|
|
||||||
|
Конфигурация пути (приоритет: env -> Airflow Variable -> default):
|
||||||
|
MIGRATIONS_REPO_DIR / Variable "migrations_repo_dir" — корень репозитория
|
||||||
|
MIGRATIONS_REPORTS_DIR / Variable "migrations_reports_dir" — общая папка отчетов
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from airflow.decorators import dag, task
|
||||||
|
|
||||||
|
|
||||||
|
def _setting(var_key: str, env_key: str, default: str) -> str:
|
||||||
|
value = os.environ.get(env_key)
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
try:
|
||||||
|
from airflow.models import Variable
|
||||||
|
return Variable.get(var_key)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
REPO_DIR = _setting("migrations_repo_dir", "MIGRATIONS_REPO_DIR", "/opt/airflow/migrations_tool")
|
||||||
|
REPORTS_DIR = _setting("migrations_reports_dir", "MIGRATIONS_REPORTS_DIR", "/opt/airflow/reports")
|
||||||
|
MIGRATIONS_DIR = os.path.join(REPO_DIR, "pgsql")
|
||||||
|
SCRIPT_PATH = os.path.join(REPO_DIR, "migration_reporter.py")
|
||||||
|
|
||||||
|
|
||||||
|
def _run(cmd: list, timeout: int = 3600) -> str:
|
||||||
|
"""Запускает команду, падает с полным выводом при ошибке"""
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Ошибка выполнения ({result.returncode}): {' '.join(map(str, cmd))}\n"
|
||||||
|
f".stdout: {result.stdout}\n.stderr: {result.stderr}"
|
||||||
|
)
|
||||||
|
return result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
@dag(
|
||||||
|
dag_id="migration_reporter_pipeline",
|
||||||
|
description="Отчет по миграциям: git pull → Excel → HTML",
|
||||||
|
schedule="0 8 * * 1,3,5", # Пн, Ср, Пт в 08:00
|
||||||
|
start_date=datetime(2026, 1, 1),
|
||||||
|
catchup=False,
|
||||||
|
max_active_runs=1,
|
||||||
|
default_args={
|
||||||
|
"owner": "migrations",
|
||||||
|
"retries": 2,
|
||||||
|
"retry_delay": timedelta(minutes=5),
|
||||||
|
},
|
||||||
|
tags=["migrations", "reporting"],
|
||||||
|
)
|
||||||
|
def migration_reporter_pipeline():
|
||||||
|
|
||||||
|
@task
|
||||||
|
def pull_repo() -> str:
|
||||||
|
"""Обновляет локальную копию репозитория миграций"""
|
||||||
|
stdout = _run(["git", "-C", REPO_DIR, "pull", "--ff-only"], timeout=600)
|
||||||
|
return stdout.strip() or "already up to date"
|
||||||
|
|
||||||
|
@task
|
||||||
|
def generate_excel() -> dict:
|
||||||
|
"""Анализ миграций + Excel-отчет; кэш изменений идет в XCom"""
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
output = os.path.join(REPORTS_DIR, f"migration_report_{timestamp}.xlsx")
|
||||||
|
stdout = _run([
|
||||||
|
sys.executable, SCRIPT_PATH,
|
||||||
|
"excel", "--dir", MIGRATIONS_DIR, "--output", output,
|
||||||
|
])
|
||||||
|
match = re.search(r"^CACHE_FILE: (.+)$", stdout, re.MULTILINE)
|
||||||
|
cache_path = match.group(1).strip() if match else output.replace(".xlsx", ".changes.json")
|
||||||
|
return {"report": output, "cache": cache_path}
|
||||||
|
|
||||||
|
@task
|
||||||
|
def generate_html(excel_info: dict) -> str:
|
||||||
|
"""HTML-отчет из кэша изменений (в файлы latest.html / latest.xlsx)."""
|
||||||
|
output = os.path.join(
|
||||||
|
REPORTS_DIR, os.path.basename(excel_info["report"]).replace(".xlsx", ".html")
|
||||||
|
)
|
||||||
|
stdout = _run([
|
||||||
|
sys.executable, SCRIPT_PATH,
|
||||||
|
"html", "--cache", excel_info["cache"], "--output", output,
|
||||||
|
])
|
||||||
|
match = re.search(r"^HTML_FILE: (.+)$", stdout, re.MULTILINE)
|
||||||
|
return match.group(1).strip() if match else output
|
||||||
|
|
||||||
|
pull = pull_repo()
|
||||||
|
excel = generate_excel()
|
||||||
|
html = generate_html(excel)
|
||||||
|
|
||||||
|
pull >> excel >> html
|
||||||
|
|
||||||
|
|
||||||
|
migration_reporter_pipeline()
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user