6033967e6a
Ship Keystone auth, multi-service handlers (Yoga→Dalmatian), Compose/Helm packaging, API contract packs, and pytest/Pulumi coverage labs.
102 lines
3.3 KiB
Python
102 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Scan GET collection endpoints for empty list payloads across all series."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from app.openstack.contract_loader import list_series, load_series_pack # noqa: E402
|
|
from app.openstack.surface_probe import ( # noqa: E402
|
|
activate_series,
|
|
fill_path,
|
|
http_request,
|
|
issue_token,
|
|
)
|
|
|
|
|
|
def _is_empty_list_payload(body: object) -> tuple[bool, str | None]:
|
|
if not isinstance(body, dict):
|
|
return False, None
|
|
if body.get("data") == []:
|
|
return True, "data"
|
|
for key, value in body.items():
|
|
if key in {"links", "metadata", "versions", "version", "id", "status"}:
|
|
continue
|
|
if isinstance(value, list) and len(value) == 0:
|
|
return True, key
|
|
return False, None
|
|
|
|
|
|
def _is_top_level_collection(op) -> bool: # noqa: ANN001
|
|
if op.method != "GET":
|
|
return False
|
|
if op.kind in {"collection", "detail"}:
|
|
return "{" not in op.path or op.path.rstrip("/").endswith("/detail")
|
|
if op.kind == "custom" and "{" not in op.path:
|
|
return True
|
|
return False
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--host", default="http://api-gateway:5000")
|
|
parser.add_argument("--series", action="append", default=[])
|
|
args = parser.parse_args()
|
|
host = args.host.rstrip("/")
|
|
if args.series:
|
|
series_list = args.series
|
|
else:
|
|
series_list = [str(item["series"]) for item in list_series()]
|
|
|
|
token, _ = issue_token(host, user="admin", project="admin")
|
|
empties: list[tuple[str, str, str, str, str | None, int, str | None]] = []
|
|
checked = 0
|
|
|
|
for series in series_list:
|
|
activate_series(host, series)
|
|
packs = load_series_pack(series)
|
|
for name, pack in sorted(packs.items()):
|
|
for op in pack.operations:
|
|
if not _is_top_level_collection(op):
|
|
continue
|
|
path = fill_path(op.path)
|
|
status, body = http_request("GET", f"{host}{path}", token=token, service=name)
|
|
checked += 1
|
|
empty, key = _is_empty_list_payload(body)
|
|
if empty:
|
|
empties.append(
|
|
(
|
|
series,
|
|
name,
|
|
op.path,
|
|
op.resource_type,
|
|
op.collection_key,
|
|
status,
|
|
key,
|
|
)
|
|
)
|
|
|
|
by_path: dict[tuple[str, str, str, str | None], list[str]] = defaultdict(list)
|
|
for series, svc, path, rtype, ckey, _status, _key in empties:
|
|
by_path[(svc, rtype, path, ckey)].append(series)
|
|
|
|
print(json.dumps({"checked": checked, "empty": len(empties), "unique": len(by_path)}, indent=2))
|
|
print("\n=== EMPTY COLLECTIONS ===")
|
|
for (svc, rtype, path, ckey), serieses in sorted(by_path.items()):
|
|
print(
|
|
f"{svc:12} {rtype:28} key={str(ckey):24} {path} "
|
|
f"series={','.join(sorted(set(serieses)))}"
|
|
)
|
|
return 0 if not by_path else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|