Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
104 lines
5 KiB
Python
104 lines
5 KiB
Python
#!/usr/bin/env python3
|
|
"""Read-only bounded S3 backup metadata through existing scoped runtime delivery.
|
|
|
|
No bootstrap API key, new grant, object content read, age key, provider write or
|
|
retention action. The caller must already have admitted cluster read authority.
|
|
"""
|
|
import argparse
|
|
import base64
|
|
from datetime import datetime, timezone
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
|
|
BUCKET='railiance-platform-pg-backup'
|
|
ENDPOINT='https://s3.nl-ams.scw.cloud'
|
|
PREFIX='platform-pg/'
|
|
CLUSTER='a553c742-0115-43d4-99a4-a5ca56fe0786'
|
|
|
|
|
|
class Refused(Exception):pass
|
|
|
|
|
|
def kube(args):
|
|
p=subprocess.run(['kubectl',*args,'-o','json'],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=30)
|
|
if p.returncode:raise Refused()
|
|
return json.loads(p.stdout)
|
|
|
|
|
|
def list_prefix(client,prefix):
|
|
if not prefix.startswith(PREFIX) or not re.fullmatch(r'[A-Za-z0-9._/-]+',prefix) or '..' in prefix.split('/'):raise Refused()
|
|
directories=[];files=[];continuation=None
|
|
for _ in range(20):
|
|
args={'Bucket':BUCKET,'Prefix':prefix,'Delimiter':'/','MaxKeys':1000}
|
|
if continuation:args['ContinuationToken']=continuation
|
|
page=client.list_objects_v2(**args)
|
|
for row in page.get('CommonPrefixes',[]):
|
|
value=row['Prefix']
|
|
if not value.startswith(prefix) or not value.endswith('/') or not re.fullmatch(r'[A-Za-z0-9._/-]+',value) or '..' in value.split('/'):raise Refused()
|
|
directories.append(value)
|
|
for row in page.get('Contents',[]):
|
|
name=row['Key']
|
|
if not name.startswith(prefix) or not re.fullmatch(r'[A-Za-z0-9._/-]+',name) or '..' in name.split('/'):raise Refused()
|
|
if name.endswith(('.age','.json','.yaml')):
|
|
size=row['Size']
|
|
if type(size) is not int or size<0:raise Refused()
|
|
files.append({'key':name,'bytes':size})
|
|
if not page.get('IsTruncated'):break
|
|
continuation=page.get('NextContinuationToken')
|
|
if not continuation:raise Refused()
|
|
else:raise Refused()
|
|
return {'directories':sorted(set(directories)),'files':sorted(files,key=lambda x:x['key'])}
|
|
|
|
|
|
def inventory(client):
|
|
pending=[(PREFIX,0)];result={}
|
|
while pending:
|
|
prefix,depth=pending.pop(0)
|
|
if prefix in result:raise Refused()
|
|
if len(result)>=64:raise Refused()
|
|
row=list_prefix(client,prefix);result[prefix]=row
|
|
if depth<3:
|
|
for child in row['directories']:
|
|
# Native database WAL/base trees are not application/key-file
|
|
# archives. Record their presence without enumerating every WAL.
|
|
if any(x in child.split('/') for x in ['wals','base']):continue
|
|
pending.append((child,depth+1))
|
|
return result
|
|
|
|
|
|
def main():
|
|
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--receipt',required=True)
|
|
a=p.parse_args();fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
|
|
r={'schema':'netkingdom.scaleway-backup-inventory.v1','status':'failed',
|
|
'started_at':datetime.now(timezone.utc).isoformat(),'read_only':True,
|
|
'provider':'Scaleway','bucket':BUCKET,'prefix':PREFIX,'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
|
'bootstrap_api_key_used':False,'age_keys_read':False,'object_contents_downloaded':False,
|
|
'secret_values_exported':False,'scope':'Bounded prefix metadata, at most three child levels; WAL/base trees not enumerated; other buckets/prefixes unassessed'}
|
|
try:
|
|
if kube(['get','namespace','kube-system'])['metadata']['uid']!=CLUSTER:raise Refused()
|
|
delivered=kube(['-n','databases','get','secret','platform-pg-backup-s3'])
|
|
if delivered['metadata']['namespace']!='databases' or delivered['metadata']['name']!='platform-pg-backup-s3':raise Refused()
|
|
values=delivered['data']
|
|
# Same governed runtime delivery used by Platform's archive verifier;
|
|
# explicit credentials avoid SDK ambient/profile/metadata discovery.
|
|
import boto3
|
|
from botocore.config import Config
|
|
client=boto3.client('s3',endpoint_url=ENDPOINT,region_name='nl-ams',
|
|
aws_access_key_id=base64.b64decode(values['ACCESS_KEY_ID'],validate=True).decode(),
|
|
aws_secret_access_key=base64.b64decode(values['ACCESS_SECRET_KEY'],validate=True).decode(),
|
|
config=Config(signature_version='s3v4',proxies={},connect_timeout=15,read_timeout=30,
|
|
retries={'max_attempts':1},s3={'addressing_style':'path'}))
|
|
r['prefixes']=inventory(client);r['status']='metadata_inventory_complete'
|
|
r['runtime_delivery_uid']=delivered['metadata']['uid']
|
|
except Exception:r['error']='inventory_failed_diagnostics_suppressed'
|
|
finally:
|
|
r['finished_at']=datetime.now(timezone.utc).isoformat()
|
|
with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n')
|
|
return int(r['status']=='failed')
|
|
|
|
|
|
if __name__=='__main__':raise SystemExit(main())
|