check-optional-release.py 4.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. """Read-only HTTP validation for the release; secrets stay in server memory."""
  2. import argparse
  3. import json
  4. import re
  5. import subprocess
  6. from pathlib import Path
  7. from urllib.error import HTTPError
  8. from urllib.request import Request, urlopen
  9. parser = argparse.ArgumentParser()
  10. parser.add_argument('--base-url', required=True)
  11. parser.add_argument('--env', type=Path, required=True)
  12. parser.add_argument('--output', type=Path, required=True)
  13. parser.add_argument('--db-container', required=True)
  14. args = parser.parse_args()
  15. settings = dict(line.split('=',1) for line in args.env.read_text().splitlines() if line and not line.startswith('#') and '=' in line)
  16. checks = []
  17. def request(path, body=None, token=None):
  18. headers={'Content-Type':'application/json'}
  19. if token: headers['Authorization']='Bearer '+token
  20. req=Request(args.base_url.rstrip('/')+path, data=json.dumps(body).encode() if body is not None else None, headers=headers)
  21. try: response=urlopen(req,timeout=25)
  22. except HTTPError as error: response=error
  23. with response:
  24. raw=response.read()
  25. try: data=json.loads(raw)
  26. except (ValueError,UnicodeDecodeError): data={}
  27. return response.status,data,raw,response.headers
  28. def check(name, okay, **details):
  29. checks.append({'name':name,'passed':bool(okay),**details})
  30. def sql(statement):
  31. return subprocess.check_output(['docker','exec',args.db_container,'psql','-U','relay','-d','relay','-At','-c',statement]).decode().strip()
  32. status,data,_,_=request('/health')
  33. check('health',status==200 and data.get('status')=='ok')
  34. status,_,page,_=request('/home')
  35. check('frontend_shell',status==200 and b'type="module"' in page)
  36. assets=re.findall(rb'(?:src|href)="(/assets/[^"\s]+)"',page)
  37. check('frontend_assets_listed',bool(assets))
  38. for asset in assets[:8]:
  39. status,_,_,_=request(asset.decode())
  40. check('asset:'+asset.decode(),status==200)
  41. for path in ['/api/v1/keys','/api/v1/admin/accounts/1/traffic-control','/api/v1/admin/groups/1/stats']:
  42. status,_,_,_=request(path)
  43. check('unauthenticated:'+path,status==401,status=status)
  44. status,_,_,_=request('/v1/responses',{'model':'deployment-auth-check','input':'hello'})
  45. check('gateway_requires_api_key',status==401,status=status)
  46. status,data,_,_=request('/api/v1/auth/login',{'email':settings['ADMIN_EMAIL'],'password':settings['ADMIN_PASSWORD']})
  47. token=(data.get('data') or {}).get('access_token')
  48. check('existing_admin_login',status==200 and bool(token),status=status)
  49. if token:
  50. status,data,_,_=request('/api/v1/user/profile',token=token)
  51. check('authenticated_profile',status==200 and data.get('code')==0,status=status)
  52. aid=sql("SELECT id FROM accounts WHERE deleted_at IS NULL ORDER BY id LIMIT 1")
  53. if aid:
  54. status,data,_,headers=request('/api/v1/admin/accounts/'+aid+'/traffic-control',token=token)
  55. value=data.get('data') or {}; policy=value.get('policy') or {}
  56. check('traffic_control_read',status==200 and isinstance(policy.get('strict_rpm_enabled'),bool) and isinstance(policy.get('adaptive_enabled'),bool) and value.get('state_available') is True and headers.get('Cache-Control')=='no-store',status=status)
  57. gid=sql("SELECT id FROM groups WHERE deleted_at IS NULL ORDER BY id LIMIT 1")
  58. if gid:
  59. status,data,_,headers=request('/api/v1/admin/groups/'+gid+'/stats',token=token)
  60. value=data.get('data') or {}
  61. required=['total_actual_cost','total_cost','total_account_cost','total_requests']
  62. check('real_group_stats',status==200 and all(isinstance(value.get(k),(int,float)) for k in required) and headers.get('Cache-Control')=='no-store',status=status)
  63. status,_,_,_=request('/api/v1/admin/groups/'+gid+'/stats?from=bad-date',token=token)
  64. check('group_stats_invalid_date',status==400,status=status)
  65. for path in ['/api/v1/admin/intelligent-tests/accounts','/api/v1/admin/intelligent-tests/settings']:
  66. status,data,_,_=request(path,token=token)
  67. check('test_center:'+path,status==200 and data.get('code')==0,status=status)
  68. result={'base_url':args.base_url,'checks':checks,'passed':all(c['passed'] for c in checks),'scope':'health, assets, auth, read-only admin APIs; no model request with real credentials'}
  69. args.output.write_text(json.dumps(result,ensure_ascii=False,indent=2))
  70. print(json.dumps(result,ensure_ascii=False,indent=2))
  71. raise SystemExit(0 if result['passed'] else 1)