CoolFace
Apppublic

altool/airflow

sourceHugging Faceupdated 7mo agoView on Hugging Face
0likes
refresh_service.py194 linesDownload Raw Back to root
1"""2DAG Refresh Service — lightweight Flask sidecar for git-syncing DAGs.3 4Endpoints:5    GET/POST /refresh  — pull latest DAGs from the configured git repo6    GET      /health   — liveness check (also pings Airflow webserver)7    GET      /config   — show current (non-sensitive) configuration8    GET      /status   — git SHA, last sync time, repo info9"""10 11import fcntl12import logging13import os14import subprocess15import time16from datetime import datetime, timezone17 18from flask import Flask, jsonify, request19 20# ---- Logging ----21logging.basicConfig(22    level=logging.INFO,23    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",24    datefmt="%Y-%m-%d %H:%M:%S",25)26logger = logging.getLogger("refresh-service")27 28app = Flask(__name__)29 30# ---- Configuration ----31AIRFLOW_HOME = os.environ.get("AIRFLOW_HOME", "/opt/airflow")32DAGS_DIR = os.path.join(AIRFLOW_HOME, "dags")33DAG_REPO_URL = os.environ.get("DAG_REPO_URL", "https://github.com/subhamgiri460/myworkflows.git")34DAG_REPO_BRANCH = os.environ.get("DAG_REPO_BRANCH", "main")35DAG_REPO_TOKEN = os.environ.get("DAG_REPO_TOKEN", "")36AIRFLOW_WEB_PORT = os.environ.get("AIRFLOW_WEB_PORT", "8080")37REFRESH_SERVICE_PORT = os.environ.get("REFRESH_SERVICE_PORT", "5000")38 39LOCK_FILE = "/tmp/dag_sync.lock"40_last_sync: dict = {}41 42 43def _get_authenticated_url() -> str:44    """Inject PAT token into the repo URL for private repos."""45    if DAG_REPO_TOKEN:46        return DAG_REPO_URL.replace("https://", f"https://{DAG_REPO_TOKEN}@")47    return DAG_REPO_URL48 49 50def _run_git(args: list[str], cwd: str | None = None, timeout: int = 60) -> subprocess.CompletedProcess:51    """Run a git command and return the result."""52    return subprocess.run(53        ["git"] + args,54        cwd=cwd,55        capture_output=True,56        text=True,57        timeout=timeout,58    )59 60 61def _get_head_sha() -> str | None:62    """Return the current HEAD SHA in the DAGs directory."""63    try:64        result = _run_git(["rev-parse", "--short", "HEAD"], cwd=DAGS_DIR, timeout=10)65        return result.stdout.strip() if result.returncode == 0 else None66    except Exception:67        return None68 69 70def _sync_repo() -> tuple[dict, int]:71    """Clone or update the DAG repository. Returns (response_body, status_code)."""72    global _last_sync73 74    os.makedirs(DAGS_DIR, exist_ok=True)75 76    auth_url = _get_authenticated_url()77    is_update = os.path.isdir(os.path.join(DAGS_DIR, ".git"))78 79    if is_update:80        # Update remote URL in case PAT token changed81        _run_git(["remote", "set-url", "origin", auth_url], cwd=DAGS_DIR)82 83        fetch = _run_git(["fetch", "origin", DAG_REPO_BRANCH], cwd=DAGS_DIR)84        if fetch.returncode != 0:85            logger.error("git fetch failed: %s", fetch.stderr)86            return {"status": "error", "message": f"git fetch failed: {fetch.stderr}"}, 50087 88        reset = _run_git(["reset", "--hard", f"origin/{DAG_REPO_BRANCH}"], cwd=DAGS_DIR)89        if reset.returncode != 0:90            logger.error("git reset failed: %s", reset.stderr)91            return {"status": "error", "message": f"git reset failed: {reset.stderr}"}, 50092        action = "updated"93    else:94        clone = _run_git(95            ["clone", "--depth", "1", "--branch", DAG_REPO_BRANCH, auth_url, DAGS_DIR],96            timeout=120,97        )98        if clone.returncode != 0:99            logger.error("git clone failed: %s", clone.stderr)100            return {"status": "error", "message": f"git clone failed: {clone.stderr}"}, 500101        action = "cloned"102 103    sha = _get_head_sha()104    _last_sync = {105        "action": action,106        "sha": sha,107        "timestamp": datetime.now(timezone.utc).isoformat(),108    }109 110    logger.info("DAGs %s — commit %s", action, sha)111    return {112        "status": "success",113        "message": f"DAGs {action} successfully",114        "repo": DAG_REPO_URL,115        "branch": DAG_REPO_BRANCH,116        "sha": sha,117    }, 200118 119 120# ---- Request logging ----121@app.before_request122def log_request():123    logger.info("%s %s", request.method, request.path)124 125 126# ---- Endpoints ----127@app.route("/refresh", methods=["GET", "POST"])128def refresh_dags():129    """Pull latest DAGs from git. Uses a file lock to prevent concurrent syncs."""130    try:131        lock_fd = open(LOCK_FILE, "w")132        acquired = False133        try:134            fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)135            acquired = True136        except BlockingIOError:137            return jsonify({"status": "busy", "message": "A sync is already in progress"}), 429138 139        if acquired:140            body, status = _sync_repo()141            fcntl.flock(lock_fd, fcntl.LOCK_UN)142            lock_fd.close()143            return jsonify(body), status144 145    except subprocess.TimeoutExpired:146        return jsonify({"status": "error", "message": "Git operation timed out"}), 504147    except Exception as e:148        logger.exception("Unexpected error during refresh")149        return jsonify({"status": "error", "message": str(e)}), 500150 151 152@app.route("/health")153def health():154    """Liveness probe — also checks if Airflow webserver is reachable."""155    airflow_ok = False156    try:157        import urllib.request158        resp = urllib.request.urlopen(f"http://127.0.0.1:{AIRFLOW_WEB_PORT}/health", timeout=5)159        airflow_ok = resp.status == 200160    except Exception:161        pass162 163    return jsonify({164        "status": "healthy",165        "airflow_webserver": "up" if airflow_ok else "starting",166    }), 200167 168 169@app.route("/config")170def show_config():171    """Return non-sensitive configuration values."""172    return jsonify({173        "dags_dir": DAGS_DIR,174        "repo_url": DAG_REPO_URL,175        "branch": DAG_REPO_BRANCH,176        "airflow_web_port": AIRFLOW_WEB_PORT,177        "refresh_service_port": REFRESH_SERVICE_PORT,178    }), 200179 180 181@app.route("/status")182def status():183    """Return the last sync status, current git SHA, and uptime."""184    return jsonify({185        "last_sync": _last_sync or "no sync yet",186        "current_sha": _get_head_sha(),187        "repo": DAG_REPO_URL,188        "branch": DAG_REPO_BRANCH,189    }), 200190 191 192if __name__ == "__main__":193    port = int(REFRESH_SERVICE_PORT)194    app.run(host="0.0.0.0", port=port)