From 34b9385bee7cc34df048c7832b33fe9c798907ba Mon Sep 17 00:00:00 2001 From: codex Date: Sat, 5 Sep 2026 22:32:30 +0200 Subject: [PATCH] Deliver validated backup capture to worker via immutable runtime bundle Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a06ecb-456a-71c2-b41e-0755d336e883 --- scripts/deploy_verified_backup_bundle.py | 46 ++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 scripts/deploy_verified_backup_bundle.py diff --git a/scripts/deploy_verified_backup_bundle.py b/scripts/deploy_verified_backup_bundle.py new file mode 100644 index 0000000..fbda62b --- /dev/null +++ b/scripts/deploy_verified_backup_bundle.py @@ -0,0 +1,46 @@ +#!/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()