KUBECTL_ROUTE (agent default, founder-sudo = ssh railiance01 sudo -n) for the bounded proof scripts; one attended script with go/no-go gates, typed ADOPT confirmation, prune/force-off SSA sync and post-checks; evaluator and tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 527080@bnt-lap001 Assistant-Session: 5a0e864a-67be-4605-a77a-16635f7dd292
130 lines
7.8 KiB
Python
130 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Guarded public-only Argo transport prerequisite; no Secret or credential calls."""
|
|
import argparse
|
|
import copy
|
|
from datetime import datetime,timezone
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
|
|
URL='ssh://git@forgejo.coulomb.social:30022/coulomb/rapp-issue-core.git'
|
|
D2='89f1058f-33bc-47a8-8a49-0b44d426c67e'
|
|
TARGETS=[('configmap','argocd-ssh-known-hosts-cm','/data/ssh_known_hosts','railiance-enablement'),('appproject','railiance-tenants','/spec/sourceRepos','railiance-platform')]
|
|
|
|
class Refused(Exception):pass
|
|
|
|
def _kube_route():
|
|
"""Local-only import; this module's source may also run remotely without it."""
|
|
import sys
|
|
here = str(Path(__file__).resolve().parent)
|
|
if here not in sys.path:
|
|
sys.path.insert(0, here)
|
|
import kube_route
|
|
return kube_route
|
|
|
|
def require(ok,reason):
|
|
if not ok:raise Refused(reason)
|
|
|
|
def digest(value):return hashlib.sha256(json.dumps(value,sort_keys=True).encode()).hexdigest()
|
|
|
|
def run(args,body=None):
|
|
try:
|
|
result=subprocess.run(_kube_route().ssh_argv(['kubectl','--request-timeout=15s',*args]),input=body,text=True,capture_output=True,timeout=60)
|
|
except Exception:raise Refused('command_outcome_unverified') from None
|
|
require(result.returncode==0,'command_outcome_unverified')
|
|
return result.stdout
|
|
|
|
def value(obj,path):
|
|
for key in path.strip('/').split('/'):obj=obj[key]
|
|
return obj
|
|
|
|
def replace(obj,path,wanted):
|
|
keys=path.strip('/').split('/');parent=obj
|
|
for key in keys[:-1]:parent=parent[key]
|
|
parent[keys[-1]]=wanted
|
|
|
|
def normalized(obj):
|
|
obj=copy.deepcopy(obj)
|
|
for field in ('managedFields','resourceVersion','generation'):obj.get('metadata',{}).pop(field,None)
|
|
return obj
|
|
|
|
def validate(plan):
|
|
require(plan.get('schema')=='argocd-public-transport/v1' and plan.get('task')=='RPF-WP-0044-T04' and plan.get('approval_ref')==D2,'plan_authority')
|
|
require(plan.get('target')=='railiance01' and plan.get('namespace')=='argocd' and plan.get('repo_server_tcp_30022_verified') is True,'plan_target')
|
|
require(all(isinstance(plan.get(k),str) and plan[k] for k in ('diff_ref','rollback','abort_owner','baseline_sha256','public_authority_observation_sha256')),'plan_receipt_contract')
|
|
require(len(plan.get('resources',[]))==2,'target_count')
|
|
for item,fixed in zip(plan['resources'],TARGETS):
|
|
require(tuple(item.get(k) for k in ('kind','name','path','owner'))==fixed,'fixed_target')
|
|
require(bool(re.fullmatch(r'[a-f0-9-]{36}',item.get('uid',''))),'target_uid')
|
|
cm,project=plan['resources']
|
|
require(isinstance(cm['before'],str) and cm['before'].endswith('\n') and isinstance(cm['after'],str),'host_field_type')
|
|
added=cm['after'][len(cm['before']):]
|
|
require(cm['after'].startswith(cm['before']) and bool(re.fullmatch(r'\[forgejo\.coulomb\.social\]:30022 (ssh-rsa|ssh-ed25519|ecdsa-sha2-nistp256) [A-Za-z0-9+/=]+\n',added)),'host_addition_scope')
|
|
require('[forgejo.coulomb.social]:30022 ' not in cm['before'],'host_preimage_already_admitted')
|
|
require(isinstance(project['before'],list) and all(isinstance(v,str) for v in project['before']) and URL not in project['before'] and project['after']==project['before']+[URL],'repository_addition_scope')
|
|
|
|
def patch(item,live,rollback=False):
|
|
before,after=(item['after'],item['before']) if rollback else (item['before'],item['after'])
|
|
require(live['metadata']['uid']==item['uid'],'target_uid_changed')
|
|
require(value(live,item['path'])==before,'target_preimage_changed')
|
|
require(isinstance(live['metadata'].get('resourceVersion'),str),'resource_version_required')
|
|
return [{'op':'test','path':'/metadata/uid','value':item['uid']},{'op':'test','path':'/metadata/resourceVersion','value':live['metadata']['resourceVersion']},{'op':'test','path':item['path'],'value':before},{'op':'replace','path':item['path'],'value':after}]
|
|
|
|
def perform(plan,action,receipt,checkpoint=lambda:None):
|
|
validate(plan)
|
|
nodes=json.loads(run(['get','nodes','-o','json']))['items']
|
|
require(len(nodes)==1 and any(a.get('address')=='92.205.62.239' for a in nodes[0]['status']['addresses']),'wrong_estate')
|
|
read=lambda item:json.loads(run(['-n','argocd','get',item['kind'],item['name'],'-o','json']))
|
|
rollback=action=='rollback'
|
|
# Validate both targets before the first write, avoiding an avoidable partial change.
|
|
live=[read(item) for item in plan['resources']]
|
|
for item,obj in zip(plan['resources'],live):
|
|
require(obj['metadata']['uid']==item['uid'],'target_uid_changed')
|
|
if action=='verify':require(value(obj,item['path'])==item['after'],'verification_field_changed')
|
|
elif rollback and value(obj,item['path'])==item['before']:continue
|
|
else:patch(item,obj,rollback)
|
|
for item,obj in zip(plan['resources'],live):
|
|
record={'kind':item['kind'],'name':item['name'],'uid':item['uid'],'field':item['path'],'before_sha256':digest(item['before']),'after_sha256':digest(item['after'])};receipt['resources'].append(record)
|
|
if action=='verify':record['verified']=True;continue
|
|
if rollback and value(obj,item['path'])==item['before']:record['already_restored']=True;continue
|
|
body=json.dumps(patch(item,obj,rollback));args=['-n','argocd','patch',item['kind'],item['name'],'--type=json','--patch-file=/dev/stdin']
|
|
admitted=json.loads(run(args+['--dry-run=server','-o','json'],body))
|
|
wanted=copy.deepcopy(obj);replace(wanted,item['path'],item['before'] if rollback else item['after'])
|
|
require(normalized(admitted)==normalized(wanted),'dry_run_modified_other_fields');record['server_dry_run']=True
|
|
if action=='dry-run':continue
|
|
receipt['pending_target']=item['kind']+'/'+item['name'];receipt['mutation_attempted']=True;checkpoint()
|
|
run(args+['-o','name'],body)
|
|
checked=read(item)
|
|
require(checked['metadata']['uid']==item['uid'] and normalized(checked)==normalized(wanted),'postchange_projection_unverified')
|
|
record['changed_verified']=True;receipt['pending_target']=None;checkpoint()
|
|
receipt['passed']=True;return receipt
|
|
|
|
def main():
|
|
parser=argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--plan',type=Path,required=True);parser.add_argument('--plan-sha256',required=True)
|
|
parser.add_argument('--action',choices=('dry-run','execute','verify','rollback'),default='dry-run')
|
|
parser.add_argument('--receipt',type=Path,required=True)
|
|
args=parser.parse_args()
|
|
receipt={'schema':'argocd-public-transport-receipt/v1','task':'RPF-WP-0044-T04','approval_ref':D2,'action':args.action,'passed':False,'mutation_attempted':False,'resources':[],'secret_or_credential_calls':False,'captured_at':datetime.now(timezone.utc).isoformat()}
|
|
# Exclusive receipt is a replay guard. Never overwrite another attempt.
|
|
fd=None
|
|
try:
|
|
require(not args.receipt.exists(),'receipt_exists_do_not_replay')
|
|
fd=os.open(args.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
|
|
raw=args.plan.read_bytes();require(hashlib.sha256(raw).hexdigest()==args.plan_sha256,'plan_changed')
|
|
def checkpoint():
|
|
os.lseek(fd,0,os.SEEK_SET);os.ftruncate(fd,0);os.write(fd,(json.dumps(receipt,sort_keys=True)+'\n').encode());os.fsync(fd)
|
|
receipt['plan_sha256']=args.plan_sha256;checkpoint();perform(json.loads(raw),args.action,receipt,checkpoint)
|
|
except Refused as error:receipt['failure']=str(error)
|
|
except Exception:receipt['failure']='internal_outcome_unverified'
|
|
finally:
|
|
if fd is not None:
|
|
os.lseek(fd,0,os.SEEK_SET);os.ftruncate(fd,0);os.write(fd,(json.dumps(receipt,sort_keys=True)+'\n').encode());os.fsync(fd);os.close(fd)
|
|
# Only whitelist receipt fields are emitted. Every raw child error stays captured.
|
|
print(json.dumps(receipt,sort_keys=True));return 0 if receipt['passed'] else 1
|
|
|
|
if __name__=='__main__':raise SystemExit(main())
|