34 lines
3.5 KiB
Python
34 lines
3.5 KiB
Python
import json, subprocess, re, datetime
|
|
|
|
def command(args, payload=None):
|
|
r=subprocess.run(['kubectl','--request-timeout=15s',*args],input=payload,capture_output=True,text=True,timeout=35)
|
|
if r.returncode: raise SystemExit('Metadata observation failed; raw output suppressed.')
|
|
return r.stdout
|
|
|
|
def ready(p):
|
|
return p.get('status',{}).get('phase')=='Running' and any(c.get('type')=='Ready' and c.get('status')=='True' for c in p.get('status',{}).get('conditions',[]))
|
|
nodes=json.loads(command(['get','nodes','-o','json']))['items']
|
|
if len(nodes)!=1 or not any(a['address']=='92.205.62.239' for a in nodes[0]['status']['addresses']): raise SystemExit('Unexpected target host.')
|
|
pods=json.loads(command(['-n','forgejo','get','pods','-o','json']))['items']
|
|
apps=[p for p in pods if ready(p) and any(c['name']=='gitea' for c in p['spec']['containers'])]
|
|
if len(apps)!=1: raise SystemExit('Ambiguous forge pod.')
|
|
app=apps[0]
|
|
cluster=json.loads(command(['-n','databases','get','cluster','forgejo-db','-o','json']))
|
|
primary=cluster.get('status',{}).get('currentPrimary')
|
|
if not primary or not re.fullmatch(r'forgejo-db-[0-9]+',primary): raise SystemExit('No unambiguous database primary.')
|
|
query='BEGIN READ ONLY;\nSET LOCAL statement_timeout = \'5s\';\nSELECT json_build_object(\n \'repositories\',(SELECT json_agg(row_to_json(x) ORDER BY x.id) FROM\n (SELECT r.id,u.lower_name AS owner,r.lower_name AS name,r.is_private,r.is_empty,r.is_archived,r.is_mirror,r.is_fork,r.object_format_name,r.git_size,r.lfs_size,\n (SELECT count(*) FROM protected_branch b WHERE b.repo_id=r.id) AS protected_branch_rules,\n (SELECT json_agg(ru.type ORDER BY ru.type) FROM repo_unit ru WHERE ru.repo_id=r.id) AS unit_types\n FROM repository r JOIN "user" u ON u.id=r.owner_id) x)\n);\nROLLBACK;'
|
|
raw=command(['-n','databases','exec','-i',primary,'-c','postgres','--','psql','-X','-U','postgres','-d','forgejo','-Atq','-v','ON_ERROR_STOP=1'],query)
|
|
metadata=json.loads(raw)
|
|
versions=command(['-n','forgejo','exec',app['metadata']['name'],'-c','gitea','--','/usr/bin/git','--version']).strip()
|
|
directories=command(['-n','forgejo','exec',app['metadata']['name'],'-c','gitea','--','find','/data/git/gitea-repositories','-mindepth','2','-maxdepth','2','-type','d','-name','*.git']).splitlines()
|
|
relative=[]
|
|
for directory in directories:
|
|
path=directory.removeprefix('/data/git/gitea-repositories/')
|
|
if not re.fullmatch(r'[a-z0-9_.-]+/[a-z0-9_.-]+\.git',path): raise SystemExit('Unrecognized repository path; output suppressed.')
|
|
relative.append(path)
|
|
for repo in metadata['repositories']:
|
|
if not re.fullmatch(r'[a-z0-9_.-]+',repo['owner']) or not re.fullmatch(r'[a-z0-9_.-]+',repo['name']): raise SystemExit('Unrecognized repository identity; output suppressed.')
|
|
repo['main_directory_present']=repo['owner']+'/'+repo['name']+'.git' in relative
|
|
repo['wiki_directory_present']=repo['owner']+'/'+repo['name']+'.wiki.git' in relative
|
|
result={'observed_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),'host':'railiance01','forge_pod':app['metadata']['name'],'forge_pod_uid':app['metadata']['uid'],'containers':[{'name':c['name'],'image':c['image'],'resources':c.get('resources',{})} for c in app['spec']['containers']],'database_primary':primary,'git_version':versions,'repositories':metadata['repositories'],'repository_directories':sorted(relative),'scope':'Read-only metadata: DB identifiers/flags/sizes/unit types/protected-rule counts and repository-directory presence. No descriptions, URLs, credentials, repository contents or logs read.'}
|
|
print(json.dumps(result,indent=2))
|