108 lines
4.0 KiB
Python
108 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Fail a deployment unless frontend and backend run the expected release."""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import urllib.request
|
|
|
|
|
|
def fetch_json(url):
|
|
with urllib.request.urlopen(url, timeout=20) as response:
|
|
if response.status != 200:
|
|
raise RuntimeError(f"{url} returned HTTP {response.status}")
|
|
return json.load(response)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("base_url", help="Public site URL, for example https://digital.99hui.com")
|
|
parser.add_argument("expected_sha", help="Full Git commit SHA being deployed")
|
|
parser.add_argument("--backend-container", help="Backend container name for image and mount checks")
|
|
parser.add_argument("--frontend-container", help="Frontend container name for image checks")
|
|
parser.add_argument("--expected-db-source", help="Required host directory mounted for SQLite and its WAL files")
|
|
parser.add_argument("--expected-upload-source", help="Required host source mounted as the upload directory")
|
|
args = parser.parse_args()
|
|
|
|
base_url = args.base_url.rstrip("/")
|
|
errors = []
|
|
try:
|
|
health = fetch_json(f"{base_url}/api/health").get("data") or {}
|
|
except Exception as exc:
|
|
errors.append(f"cannot read backend release metadata: {exc}")
|
|
health = {}
|
|
try:
|
|
frontend = fetch_json(f"{base_url}/version.json")
|
|
except Exception as exc:
|
|
errors.append(f"cannot read frontend release metadata: {exc}")
|
|
frontend = {}
|
|
|
|
if health.get("status") != "ok":
|
|
errors.append(f"backend status is {health.get('status')!r}")
|
|
failed_checks = [name for name, passed in (health.get("checks") or {}).items() if not passed]
|
|
if failed_checks:
|
|
errors.append("backend checks failed: " + ", ".join(failed_checks))
|
|
if health.get("gitSha") != args.expected_sha:
|
|
errors.append(f"backend SHA is {health.get('gitSha')!r}")
|
|
if frontend.get("gitSha") != args.expected_sha:
|
|
errors.append(f"frontend SHA is {frontend.get('gitSha')!r}")
|
|
|
|
if args.backend_container:
|
|
backend = inspect_container(args.backend_container, errors)
|
|
check_container_revision(backend, args.expected_sha, "backend", errors)
|
|
check_mount(backend, args.expected_db_source, "database", errors)
|
|
check_mount(backend, args.expected_upload_source, "uploads", errors)
|
|
elif args.expected_db_source or args.expected_upload_source:
|
|
errors.append("--backend-container is required when checking data mounts")
|
|
|
|
if args.frontend_container:
|
|
frontend_container = inspect_container(args.frontend_container, errors)
|
|
check_container_revision(frontend_container, args.expected_sha, "frontend", errors)
|
|
|
|
if errors:
|
|
print("Deployment verification failed:", file=sys.stderr)
|
|
for error in errors:
|
|
print(f"- {error}", file=sys.stderr)
|
|
return 1
|
|
|
|
print(f"Deployment verified: {args.expected_sha}")
|
|
print("Backend checks: database, uploads, pdfOcr")
|
|
return 0
|
|
|
|
|
|
def inspect_container(name, errors):
|
|
try:
|
|
output = subprocess.check_output(
|
|
["docker", "inspect", name], universal_newlines=True
|
|
)
|
|
return json.loads(output)[0]
|
|
except Exception as exc:
|
|
errors.append(f"cannot inspect container {name!r}: {exc}")
|
|
return {}
|
|
|
|
|
|
def check_container_revision(container, expected_sha, label, errors):
|
|
actual = ((container.get("Config") or {}).get("Labels") or {}).get(
|
|
"org.opencontainers.image.revision"
|
|
)
|
|
if actual != expected_sha:
|
|
errors.append(f"{label} container image SHA is {actual!r}")
|
|
|
|
|
|
def check_mount(container, expected_source, label, errors):
|
|
if not expected_source:
|
|
return
|
|
expected = os.path.realpath(expected_source)
|
|
sources = {
|
|
os.path.realpath(mount.get("Source", ""))
|
|
for mount in container.get("Mounts") or []
|
|
}
|
|
if expected not in sources:
|
|
errors.append(f"{label} mount source {expected!r} is not attached")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|