#!/usr/bin/env python3 """Deploy the reviewed provider enrollment hook while preserving runtime secrets.""" import argparse import copy import hashlib import json from pathlib import Path import subprocess CLUSTER='a553c742-0115-43d4-99a4-a5ca56fe0786' PROVIDER='58c7f96d-61cb-4dd4-bca2-54661c0ac375' CONFIG='423d857b-6b10-40ad-aba8-2cda2d3645ef' SUFFIX='\n# P06: fail startup if the enrollment guard is unavailable.\nfrom keycape_onboarding_guard import check as _p06_onboarding_check\nPI_INIT_CHECK_HOOK = "keycape_onboarding_guard.check"\n' def kube(args,payload=None): result=subprocess.run(['kubectl','--request-timeout=20s',*args],input=json.dumps(payload) if payload is not None else None,capture_output=True,text=True,timeout=30) if result.returncode:raise RuntimeError('kubernetes_operation_failed') return json.loads(result.stdout) def plan(source,config,deployment): code=(source/'scripts/keycape_onboarding_guard.py').read_text() digest=hashlib.sha256(code.encode()).hexdigest();name='keycape-onboarding-'+digest[:16] cm={'apiVersion':'v1','kind':'ConfigMap','metadata':{'name':name,'namespace':'mfa'},'immutable':True,'data':{'keycape_onboarding_guard.py':code}} original=config['data']['pi.cfg'] if 'PI_INIT_CHECK_HOOK' in original and not original.endswith(SUFFIX):raise RuntimeError('existing_hook_requires_review') updated=original if original.endswith(SUFFIX) else original+SUFFIX spec=copy.deepcopy(deployment['spec']['template']['spec']) for container in spec['containers']: if container['name'] not in {'privacyidea','factor-recovery'}:continue env=container.setdefault('env',[]) path=next((e for e in env if e['name']=='PYTHONPATH'),None) if path and path.get('value') not in {'/opt/keycape-onboarding'}:raise RuntimeError('existing_python_path_requires_review') if not path:env.append({'name':'PYTHONPATH','value':'/opt/keycape-onboarding'}) mounts=container.setdefault('volumeMounts',[]) existing=next((m for m in mounts if m['name']=='onboarding-guard'),None) desired={'name':'onboarding-guard','mountPath':'/opt/keycape-onboarding','readOnly':True} if existing and existing!=desired:raise RuntimeError('existing_guard_mount_differs') if not existing:mounts.append(desired) spec['volumes']=[v for v in spec['volumes'] if v['name']!='onboarding-guard']+[{'name':'onboarding-guard','configMap':{'name':name}}] return cm,updated,spec def main(): parser=argparse.ArgumentParser();parser.add_argument('--source',type=Path,required=True);parser.add_argument('--apply',action='store_true');args=parser.parse_args() if kube(['get','namespace','kube-system','-o','json'])['metadata']['uid']!=CLUSTER:raise RuntimeError('cluster_changed') config=kube(['-n','mfa','get','configmap','privacyidea-cfg','-o','json']);deployment=kube(['-n','mfa','get','deployment','privacyidea','-o','json']) if config['metadata']['uid']!=CONFIG or deployment['metadata']['uid']!=PROVIDER:raise RuntimeError('target_identity_changed') cm,updated,spec=plan(args.source,config,deployment) print(json.dumps({'apply':args.apply,'guard_config':cm['metadata']['name'],'target':'mfa/privacyidea','config_uid':CONFIG,'provider_uid':PROVIDER,'secret_values_emitted':False}),flush=True) if not args.apply:return committed=subprocess.run(['git','show','HEAD:scripts/keycape_onboarding_guard.py'],cwd=args.source,capture_output=True,text=True) if committed.returncode or committed.stdout!=cm['data']['keycape_onboarding_guard.py']:raise RuntimeError('guard_source_must_be_committed') kube(['apply','-f','-','-o','json'],cm) if updated!=config['data']['pi.cfg']: kube(['-n','mfa','patch','configmap','privacyidea-cfg','--type=json','--patch-file=/dev/stdin','-o','json'],[ {'op':'test','path':'/metadata/uid','value':CONFIG},{'op':'test','path':'/metadata/resourceVersion','value':config['metadata']['resourceVersion']}, {'op':'replace','path':'/data/pi.cfg','value':updated}]) kube(['-n','mfa','patch','deployment','privacyidea','--type=json','--patch-file=/dev/stdin','-o','json'],[ {'op':'test','path':'/metadata/uid','value':PROVIDER},{'op':'test','path':'/metadata/resourceVersion','value':deployment['metadata']['resourceVersion']}, {'op':'replace','path':'/spec/template/spec','value':spec}]) after=kube(['-n','mfa','get','configmap','privacyidea-cfg','-o','json']) if after['data']!=dict(config['data'],**{'pi.cfg':updated}):raise RuntimeError('configuration_readback_changed') print(json.dumps({'guard_deployed':True,'wait_for_rollout_and_policy_activation':True})) if __name__=='__main__':main()