net-kingdom/tools/bootstrap-recovery/prepare-identity-coverage.py

95 lines
4.6 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Prepare exact identity backup coverage from read-only workload/PVC metadata.
This produces a proposal, never reads Secrets, executes in pods, changes cluster
resources, captures private files, uploads, schedules or grants credentials.
"""
import argparse
from datetime import datetime,timezone
import hashlib
import json
import os
from pathlib import Path
import subprocess
ROOT=Path(__file__).resolve().parents[2]
CLUSTER_UID='a553c742-0115-43d4-99a4-a5ca56fe0786'
PROFILE=ROOT/'tools/bootstrap-recovery/identity-backup-profile.json'
class Refused(Exception):pass
def query(namespace,kind,name):
# Closed read-only types; Secret/ConfigMap data and arbitrary API paths have
# no transport in this preparation command.
if kind not in {'namespace','deployment','pvc'}:raise Refused()
args=['kubectl','--request-timeout=20s']
if namespace:args+=['-n',namespace]
r=subprocess.run(args+['get',kind,name,'-o','json'],capture_output=True,timeout=25)
if r.returncode:raise Refused()
return json.loads(r.stdout)
def references(spec):
secrets=set();configmaps=set();pvcs=set()
for c in spec.get('containers',[])+spec.get('initContainers',[]):
for e in c.get('env',[]):
v=e.get('valueFrom',{})
if 'secretKeyRef' in v:secrets.add(v['secretKeyRef']['name'])
if 'configMapKeyRef' in v:configmaps.add(v['configMapKeyRef']['name'])
for e in c.get('envFrom',[]):
if 'secretRef' in e:secrets.add(e['secretRef']['name'])
if 'configMapRef' in e:configmaps.add(e['configMapRef']['name'])
for v in spec.get('volumes',[]):
if 'secret' in v:secrets.add(v['secret']['secretName'])
if 'configMap' in v:configmaps.add(v['configMap']['name'])
if 'persistentVolumeClaim' in v:pvcs.add(v['persistentVolumeClaim']['claimName'])
for p in v.get('projected',{}).get('sources',[]):
if 'secret' in p:secrets.add(p['secret']['name'])
if 'configMap' in p:configmaps.add(p['configMap']['name'])
return secrets,configmaps,pvcs
def prepare(profile,get=query):
if get(None,'namespace','kube-system')['metadata']['uid']!=CLUSTER_UID:raise Refused()
bindings=[]
for component in profile['components']:
ns=component['namespace'];name=component['deployment']
d=get(ns,'deployment',name);m=d['metadata']
if (m['uid']!=component['deployment_uid'] or m.get('deletionTimestamp') or
d['status'].get('observedGeneration')!=m['generation'] or d['status'].get('readyReplicas')!=1):raise Refused()
refs=references(d['spec']['template']['spec'])
expected_pvcs={p['name']:p['uid'] for p in component['pvc_refs']}
if refs[0]!=set(component['secret_refs']) or refs[1]!=set(component['configmap_refs']) or refs[2]!=set(expected_pvcs):raise Refused()
pvcs=[]
for pvc,expected in expected_pvcs.items():
obj=get(ns,'pvc',pvc)
if obj['metadata']['uid']!=expected or obj['metadata'].get('deletionTimestamp') or obj['status'].get('phase')!='Bound':raise Refused()
pvcs.append({'name':pvc,'uid':expected})
bindings.append({'namespace':ns,'deployment':name,'uid':m['uid'],'generation':m['generation'],
'pvc_bindings':pvcs,'secret_names_only':sorted(refs[0]),'configmap_names_only':sorted(refs[1])})
if get(None,'namespace','kube-system')['metadata']['uid']!=CLUSTER_UID:raise Refused()
return {'schema':'netkingdom.identity-backup-preparation.v1','status':'prepared_not_admitted',
'observed_at':datetime.now(timezone.utc).isoformat(),'cluster_uid':CLUSTER_UID,
'profile_sha256':hashlib.sha256(PROFILE.read_bytes()).hexdigest(),
'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'bindings':bindings,'secret_values_read':False,'backup_captured':False,'uploaded':False,
'runtime_changed':False,'credential_grant_created':False,
'owner_admission_required':profile['admission_gates']}
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--receipt',required=True)
a=p.parse_args()
try:
r=prepare(json.loads(PROFILE.read_text()))
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n')
except Exception:
print('ERROR: coverage preparation refused; private diagnostics suppressed.');return 1
print('OK: exact coverage proposal prepared; capture, delivery and owner admission remain pending.');return 0
if __name__=='__main__':raise SystemExit(main())