File size: 4,307 Bytes
bde2f3a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#!/usr/bin/env python3
"""Qdrant collection audit + cleanup for test_col_* artifacts.

T15 (G14 FIX) — Qdrant accumulates test_col_* collections from integration
tests that forget to clean up. This script audits and cleans them.

Usage:
    # Audit only (safe — no changes):
    python scripts/ops/qdrant_audit.py
    python scripts/ops/qdrant_audit.py --audit-only

    # Audit + cleanup (drops test_col_*):
    python scripts/ops/qdrant_audit.py --cleanup

    # Custom Qdrant URL:
    python scripts/ops/qdrant_audit.py --url http://qdrant.example:6333 --cleanup

    # CI gate mode (exit 1 if any test_col_* found):
    python scripts/ops/qdrant_audit.py --check

Cron suggestion (netcup):
    # Daily audit, notify if anything leaked:
    0 6 * * * python3 /root/scripts/qdrant_audit.py --audit-only | \\
        curl -s --data-binary @- http://localhost:9080/rmi-info || true
"""

from __future__ import annotations

import argparse
import json
import sys
import urllib.request


def fetch_collections(base_url: str) -> list[dict]:
    with urllib.request.urlopen(f"{base_url}/collections", timeout=10) as r:
        return json.loads(r.read()).get("result", {}).get("collections", [])


def fetch_collection_info(base_url: str, name: str) -> dict:
    try:
        with urllib.request.urlopen(
            f"{base_url}/collections/{name}", timeout=5
        ) as r:
            return json.loads(r.read()).get("result", {})
    except Exception as e:
        return {"error": str(e)}


def delete_collection(base_url: str, name: str) -> bool:
    req = urllib.request.Request(
        f"{base_url}/collections/{name}", method="DELETE"
    )
    try:
        with urllib.request.urlopen(req, timeout=10) as r:
            return json.loads(r.read()).get("status") == "ok"
    except Exception:
        return False


def main() -> int:
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument(
        "--url",
        default="http://localhost:6333",
        help="Qdrant base URL (default: http://localhost:6333)",
    )
    p.add_argument(
        "--audit-only",
        action="store_true",
        help="Only audit — never delete anything",
    )
    p.add_argument(
        "--cleanup",
        action="store_true",
        help="Audit + drop all test_col_* collections",
    )
    p.add_argument(
        "--check",
        action="store_true",
        help="CI mode: exit 1 if any test_col_* found (no output unless found)",
    )
    args = p.parse_args()

    cols = fetch_collections(args.url)
    test_like = [c["name"] for c in cols if c["name"].startswith("test_col")]
    production = [c["name"] for c in cols if not c["name"].startswith("test_col")]

    if args.check:
        if test_like:
            print(
                f"FAIL: {len(test_like)} test_col_* collection(s) found: "
                f"{test_like}",
                file=sys.stderr,
            )
            return 1
        return 0

    print(f"=== Qdrant Audit: {args.url} ===")
    print(f"Total collections: {len(cols)}")
    print(f"  Production: {len(production)}")
    print(f"  Test artifacts (test_col_*): {len(test_like)}")

    if not test_like:
        print("\nCLEAN — zero test_col_* artifacts.")
        return 0

    print("\n--- Test artifacts detail ---")
    for name in test_like:
        info = fetch_collection_info(args.url, name)
        points = info.get("points_count", "?")
        vectors_size = (
            info.get("config", {}).get("params", {}).get("vectors", {}).get("size", "?")
        )
        print(f"  {name}: {points} points, {vectors_size}-dim vectors")

    if args.audit_only:
        print(f"\n(Use --cleanup to drop these {len(test_like)} collection(s))")
        return 0

    if args.cleanup:
        print("\n--- Cleanup ---")
        dropped = 0
        for name in test_like:
            if delete_collection(args.url, name):
                print(f"  ✓ dropped {name}")
                dropped += 1
            else:
                print(f"  ✗ FAILED to drop {name}")
        print(f"\n{dropped}/{len(test_like)} dropped.")
        return 0 if dropped == len(test_like) else 1

    print("\n(Use --cleanup to drop these, or --audit-only to suppress this hint)")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())