the-custodian/docs/changes/CUST-WP-0080/deploy_forge_custody.py

121 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""Conditional startup-guard rollout, with real installed-scanner receive probes."""
import argparse
import datetime
import hashlib
import importlib.util
import json
from pathlib import Path
import time
HERE = Path(__file__).parent
s = importlib.util.spec_from_file_location('template', HERE / 'activate_forge_template.py')
t = importlib.util.module_from_spec(s)
s.loader.exec_module(t)
def native_probe(pod, suffix):
base = '/tmp/custodian-installed-custody-' + suffix
created = False
try:
t.native.execute(pod, 'sh', '-c', 'umask 077; test ! -e "$1" && mkdir "$1" && printf "%s" custody-probe > "$1/.owner"', 'sh', base)
created = True
raw = t.native.execute(pod, 'sh', '-s', '--', base, pod['existing_dispatcher'],
payload=(HERE / 'probe_forge_installed.sh').read_bytes(), timeout=90)
return json.loads(raw)
finally:
if created:
t.native.execute(pod, 'sh', '-c', 'test "$(cat "$1/.owner")" = custody-probe && rm -rf -- "$1" && test ! -e "$1"', 'sh', base)
def patch_for(d, candidate, rollback=False):
current = d['spec']['template']['spec']['initContainers']
matches = [i for i,c in enumerate(current) if c['name'] == candidate['name']]
patch = [{'op':'test', 'path':'/metadata/uid', 'value':d['metadata']['uid']},
{'op':'test', 'path':'/metadata/resourceVersion', 'value':d['metadata']['resourceVersion']}]
if rollback:
if matches != [0] or current[0] != candidate:
raise ValueError('rollback-candidate-differs')
patch.append({'op':'remove', 'path':'/spec/template/spec/initContainers/0'})
else:
if matches:
raise ValueError('candidate-already-present')
# apply_locally's JSON array add support appends only; replace exact guarded list.
patch.append({'op':'replace', 'path':'/spec/template/spec/initContainers', 'value':[candidate,*current]})
return patch
def verify(pod, suffix):
t.check_artifacts(pod)
stats = []
for item in pod['artifacts'][:2]:
stat = t.native.execute(pod, 'stat', '-c', '%a %u %g', pod['version_directory']+'/'+item['name']).decode().strip()
if stat != '755 1000 1000':
raise ValueError('custody-mode-wrong')
stats.append({'name':item['name'], 'stat':stat})
return {'artifacts':stats, 'receive_probe':native_probe(pod, suffix)}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--proposal',type=Path,required=True)
parser.add_argument('--receipt',type=Path,required=True)
group=parser.add_mutually_exclusive_group()
group.add_argument('--apply',action='store_true')
group.add_argument('--rollback',action='store_true')
group.add_argument('--restart-proof',action='store_true')
args=parser.parse_args()
p=json.loads(args.proposal.read_text())
for file,expected in p['executor_hashes'].items():
if hashlib.sha256((HERE/file).read_bytes()).hexdigest()!=expected:
raise ValueError('executor-changed')
original=json.loads((HERE/'receive-hook-proposal.json').read_text())
c=json.loads((HERE/'forge-custody-init.json').read_text())
d=t.deployment()
expected=p['patched_template_digest'] if args.rollback or args.restart_proof else p['original_template_digest']
if d['metadata']['uid']!=p['deployment_uid'] or t.digest(d['spec']['template'])!=expected:
raise ValueError('deployment-changed')
pod=t.target_pod(d,original)
if args.restart_proof:
patch=[{'op':'test','path':'/metadata/resourceVersion','value':d['metadata']['resourceVersion']},
{'op':'add','path':'/spec/template/metadata/annotations/custodian.coulomb.social~1custody-proof','value':p['restart_marker']}]
else:
patch=patch_for(d,c,args.rollback)
local=t.apply_locally(d,patch)
dry=t.patch_remote(patch,dry_run=True)
if t.digest(local['spec']['template'])!=t.digest(dry['spec']['template']):
raise ValueError('dry-run-differs')
receipt={'proposal_id':p['proposal_id'],'revision':p['revision'],
'proposal_sha256':hashlib.sha256(args.proposal.read_bytes()).hexdigest(),
'at':datetime.datetime.now(datetime.timezone.utc).isoformat(),
'mode':'rollback' if args.rollback else 'restart-proof' if args.restart_proof else 'apply' if args.apply else 'dry-run',
'dry_run_verified':True,'before_pod_uid':pod['pod_uid'],'result':'prepared'}
args.receipt.write_text(json.dumps(receipt,indent=2)+'\n')
if not (args.apply or args.rollback or args.restart_proof):
receipt['before_probe']=verify(pod,'before')
receipt['result']='dry-run-verified'
else:
applied=t.patch_remote(patch)
receipt.update(result='rollout-started', generation=applied['metadata']['generation'])
args.receipt.write_text(json.dumps(receipt,indent=2)+'\n')
after,new=t.wait_ready(original,t.TEMPLATE)
if new['pod_uid']==pod['pod_uid'] or t.digest(after['spec']['template'])!=t.digest(local['spec']['template']):
raise ValueError('replacement-not-exact')
receipt['after_pod_uid']=new['pod_uid']
if args.rollback:
# Removing the init guard permits fsGroup to widen modes again; repair explicitly.
t.native.execute(new,'sh','-s','--','apply',payload=(HERE/'forge-custody-guard.sh').read_bytes())
else:
raw=json.loads(t.native.command(['-n','forgejo','get','pod',new['pod'],'-o','json']))
status=next(x for x in raw['status']['initContainerStatuses'] if x['name']==c['name'])
if status['state']['terminated']['exitCode']!=0:
raise ValueError('guard-not-completed')
receipt['guard_completed']=True
receipt['after_probe']=verify(new,'after-'+str(after['metadata']['generation']))
receipt['result']='verified'
args.receipt.write_text(json.dumps(receipt,indent=2)+'\n')
print(json.dumps(receipt))
if __name__=='__main__':
main()