#!/usr/bin/env python3 """Deploy the reviewed backup entry point to activity-core without editing host checkouts.""" import argparse import hashlib import json from pathlib import Path import subprocess from state_hub_preflight_lane import assert_cluster ROOT=Path(__file__).resolve().parents[1] FILES=['tools/cmd/forgejo-backup','scripts/capture_forgejo_archive.py','lib/railiance-backup-common.sh','lib/railiance-print.sh'] def bundle(): values={p.replace('/','__'):(ROOT/p).read_text() for p in FILES} values['entrypoint']='#!/bin/sh\nexec /opt/railiance-backup-verified/tools/cmd/forgejo-backup "$@"\n' digest=hashlib.sha256(json.dumps(values,sort_keys=True).encode()).hexdigest() name='backup-verified-'+digest[:12] cm={'apiVersion':'v1','kind':'ConfigMap','metadata':{'name':name,'namespace':'activity-core','labels':{'railiance.io/workplan':'RPF-WP-0029'}},'immutable':True,'data':values} patch={'spec':{'template':{'spec':{'volumes':[{'name':'backup-verified','configMap':{'name':name,'defaultMode':365,'items':[{'key':p.replace('/','__'),'path':p} for p in FILES]+[{'key':'entrypoint','path':'entrypoint'}]}}], 'containers':[{'name':'worker','volumeMounts':[{'name':'backup-verified','mountPath':'/opt/railiance-backup-verified','readOnly':True},{'name':'backup-verified','mountPath':'/opt/railiance-platform/tools/cmd/forgejo-backup','subPath':'entrypoint','readOnly':True}]}]}}}} return cm,patch def main(): p=argparse.ArgumentParser(description=__doc__);p.add_argument('--kubeconfig',required=True);p.add_argument('--receipt',required=True);a=p.parse_args() k=['kubectl','--kubeconfig',a.kubeconfig] assert_cluster(k) cm,patch=bundle() def cmd(args,payload=None): r=subprocess.run(k+args,input=None if payload is None else json.dumps(payload).encode(),capture_output=True,timeout=180) if r.returncode: raise RuntimeError('backup bundle operation failed') return r.stdout deployment=json.loads(cmd(['-n','activity-core','get','deployment','actcore-worker','-o','json'])) # Select the existing worker container by its declared backup hostPath mount. selected=[c['name'] for c in deployment['spec']['template']['spec']['containers'] if any(m['mountPath']=='/opt/railiance-platform' for m in c.get('volumeMounts',[]))] if len(selected)!=1: raise RuntimeError('ambiguous worker container') patch['spec']['template']['spec']['containers'][0]['name']=selected[0] cmd(['apply','--dry-run=server','-f','-'],cm) cmd(['-n','activity-core','patch','deployment','actcore-worker','--type=strategic','--dry-run=server','--patch-file=/dev/stdin'],patch) cmd(['apply','-f','-'],cm) cmd(['-n','activity-core','patch','deployment','actcore-worker','--type=strategic','--patch-file=/dev/stdin'],patch) cmd(['-n','activity-core','rollout','status','deployment/actcore-worker','--timeout=150s']) for file in FILES: actual=cmd(['-n','activity-core','exec','deployment/actcore-worker','-c',selected[0],'--','sha256sum','/opt/railiance-backup-verified/'+file]).split()[0].decode() if actual!=hashlib.sha256((ROOT/file).read_bytes()).hexdigest(): raise RuntimeError('worker source mismatch') receipt={'status':'verified','configmap':cm['metadata']['name'],'worker_ready':True,'bundle_files_match':True,'host_checkout_edited':False,'rollback':'Restore deployment from its prior ReplicaSet with kubectl rollout undo; retain immutable ConfigMap until no revision references it.'} Path(a.receipt).write_text(json.dumps(receipt,indent=2)+'\n') print(json.dumps(receipt)) if __name__=='__main__': main()