139 lines
8.4 KiB
Python
139 lines
8.4 KiB
Python
"""Prepare public source and conditional patch; only read/server dry-run remotely.
|
|
|
|
Reuse the existing Forgejo template patch simulator, metadata transport and
|
|
server-dry-run helper. Never replay historical Helm release values.
|
|
"""
|
|
from pathlib import Path
|
|
import copy
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import re
|
|
|
|
import yaml
|
|
|
|
HERE=Path(__file__).resolve().parent
|
|
REPO=HERE.parents[3]
|
|
OLD=REPO/'docs/changes/CUST-WP-0080'
|
|
OLD_DIRECTORY='/data/security/receive-secret-scan/20261012-57a0642b6251'
|
|
OLD_HASH='57a0642b625105440fe146fceb4d244fd2dd08a75beb316831675643e6cc27ec'
|
|
OLD_CM='forgejo-secret-template-b1d7cf8cd13f'
|
|
|
|
def sha(raw):
|
|
return hashlib.sha256(raw).hexdigest()
|
|
|
|
def support():
|
|
spec=importlib.util.spec_from_file_location('existing_forge_template',OLD/'activate_forge_template.py')
|
|
module=importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
def replacement_patch(deployment, old_command, new_command, old_cm, new_cm):
|
|
spec=deployment['spec']['template']['spec']
|
|
guards=[i for i,c in enumerate(spec['initContainers']) if c['name']=='verify-secret-scanner']
|
|
volumes=[i for i,v in enumerate(spec['volumes']) if v['name']=='secret-scan-template']
|
|
if len(guards)!=1 or len(volumes)!=1:
|
|
raise ValueError('missing-or-ambiguous-existing-control')
|
|
command_path='/spec/template/spec/initContainers/'+str(guards[0])+'/command'
|
|
volume_path='/spec/template/spec/volumes/'+str(volumes[0])+'/configMap/name'
|
|
if spec['initContainers'][guards[0]]['command']!=old_command or spec['volumes'][volumes[0]]['configMap']['name']!=old_cm:
|
|
raise ValueError('existing-control-drift')
|
|
return [
|
|
{'op':'test','path':'/metadata/uid','value':deployment['metadata']['uid']},
|
|
{'op':'test','path':'/metadata/resourceVersion','value':deployment['metadata']['resourceVersion']},
|
|
{'op':'test','path':command_path,'value':old_command},
|
|
{'op':'test','path':volume_path,'value':old_cm},
|
|
{'op':'replace','path':command_path,'value':new_command},
|
|
{'op':'replace','path':volume_path,'value':new_cm},
|
|
]
|
|
|
|
def main():
|
|
t=support()
|
|
manifest=json.loads((HERE/'publication-inputs.json').read_text())
|
|
commit=json.loads((HERE/'publication-commit.json').read_text())['commit']
|
|
assert commit=='ea65606c1298db782f721f9443bafeb03cdef138'
|
|
source=Path('/home/worsch/railiance-forge')
|
|
for row in manifest['files']:
|
|
assert sha((source/row['path']).read_bytes())==row['sha256']
|
|
binary=Path('/tmp/custodian-receive-scanner-batch-r1/candidate-hook')
|
|
binary_hash=sha(binary.read_bytes())
|
|
assert binary_hash=='73ee04078786801fc74d80d2bef122b7764c93856e403e8de5b078336d7f9e1a'
|
|
new_directory='/data/security/receive-secret-scan/20261018-'+binary_hash[:12]
|
|
apps=Path('/home/worsch/railiance-apps')
|
|
values_text=(apps/'helm/forgejo-values.yaml').read_text()
|
|
cm_text=(apps/'manifests/forgejo-secret-scan-template.yaml').read_text()
|
|
original_values=yaml.safe_load(values_text)
|
|
original_cm=yaml.safe_load(cm_text)
|
|
assert original_cm['metadata']['name']==OLD_CM and original_cm['immutable'] is True
|
|
old_hook=original_cm['data']['hook']
|
|
assert sha(old_hook.encode())=='b1d7cf8cd13feafe44e45876b1de3fdd9c88339d8d45791fa2b9f72ca832932b'
|
|
new_hook=old_hook.replace(OLD_DIRECTORY,new_directory)
|
|
hook_hash=sha(new_hook.encode())
|
|
new_cm='forgejo-secret-template-'+hook_hash[:12]
|
|
new_values_text=values_text.replace(OLD_DIRECTORY,new_directory).replace(OLD_HASH,binary_hash).replace(OLD_CM,new_cm)
|
|
new_cm_text=cm_text.replace(OLD_DIRECTORY,new_directory).replace(OLD_CM,new_cm).replace('FORGE-WP-0006','FORGE-WP-0010')
|
|
new_values=yaml.safe_load(new_values_text)
|
|
new_configmap=yaml.safe_load(new_cm_text)
|
|
assert new_configmap['data']['hook']==new_hook
|
|
assert len(original_values['preExtraInitContainers'])==len(new_values['preExtraInitContainers'])==1
|
|
old_command=original_values['preExtraInitContainers'][0]['command']
|
|
new_command=new_values['preExtraInitContainers'][0]['command']
|
|
# Prove the source changes only the existing guard command and CM name.
|
|
expected=copy.deepcopy(original_values)
|
|
expected['preExtraInitContainers'][0]['command']=new_command
|
|
expected['extraVolumes'][0]['configMap']['name']=new_cm
|
|
assert expected==new_values
|
|
config_expected=copy.deepcopy(original_cm)
|
|
config_expected['metadata']['name']=new_cm
|
|
config_expected['metadata']['labels']['custodian.coulomb.social/workplan']='FORGE-WP-0010'
|
|
config_expected['data']['hook']=new_hook
|
|
assert config_expected==new_configmap
|
|
nodes=json.loads(t.native.command(['get','nodes','-o','json']))['items']
|
|
assert len(nodes)==1 and any(a['type']=='InternalIP' and a['address']=='92.205.62.239' for a in nodes[0]['status']['addresses'])
|
|
d=t.deployment()
|
|
assert d['metadata']['uid']=='e8eff0aa-64bb-4b44-9a80-a66612986355'
|
|
assert d['spec']['replicas']==1 and d['spec']['strategy']['type']=='Recreate'
|
|
before=t.digest(d['spec']['template'])
|
|
forward=replacement_patch(d,old_command,new_command,OLD_CM,new_cm)
|
|
simulated=t.apply_locally(d,forward)
|
|
# Rollback reads fresh metadata, verifies exact new fields, restores the two
|
|
# original fields. This simulation changes no image, environment or mounts.
|
|
reverse=replacement_patch(simulated,new_command,old_command,new_cm,OLD_CM)
|
|
restored=t.apply_locally(simulated,reverse)
|
|
assert restored==d
|
|
dry=t.patch_remote(forward,dry_run=True)
|
|
assert t.digest(dry['spec']['template'])==t.digest(simulated['spec']['template'])
|
|
dry_cm=json.loads(t.native.command(['apply','--server-side','--dry-run=server','-f','-','-o','json'],payload=json.dumps(new_configmap).encode()))
|
|
assert dry_cm['immutable'] is True and dry_cm['data']['hook']==new_hook
|
|
proposal=HERE/'native-preparation'
|
|
proposal.mkdir(exist_ok=False)
|
|
for path,text in [('helm/forgejo-values.yaml',new_values_text),('manifests/forgejo-secret-scan-template.yaml',new_cm_text)]:
|
|
output=proposal/'apps-source'/path
|
|
output.parent.mkdir(parents=True,exist_ok=True); output.write_text(text)
|
|
(proposal/'deployment-forward.json').write_text(json.dumps(forward,indent=2)+'\n')
|
|
(proposal/'configmap.json').write_text(json.dumps(new_configmap,indent=2)+'\n')
|
|
(proposal/'old-hook').write_text(old_hook)
|
|
(proposal/'new-hook').write_text(new_hook)
|
|
result={
|
|
'proposal_id':'FORGE10-RECEIVE-BATCH-20261018','revision':'preparation-1',
|
|
'source_commit':commit,'candidate_binary_sha256':binary_hash,'new_directory':new_directory,
|
|
'old_directory':OLD_DIRECTORY,'old_binary_sha256':OLD_HASH,
|
|
'old_configmap':OLD_CM,'new_configmap':new_cm,'old_hook_sha256':sha(old_hook.encode()),'new_hook_sha256':hook_hash,
|
|
'deployment_uid':d['metadata']['uid'],'generation':d['metadata']['generation'],
|
|
'original_template_sha256':before,'proposed_template_sha256':t.digest(simulated['spec']['template']),
|
|
'source_fields_only_verified':True,'conditional_forward_and_inverse_verified':True,
|
|
'deployment_server_dry_run_verified':True,'configmap_server_dry_run_verified':True,
|
|
'native_changed':False,'approval_requested':False,
|
|
'remaining':['Pinned activation/recovery executor, drift/refusal checks and resource preflight',
|
|
'Independent owner/package admission and exact founder go',
|
|
'Native exact-binary smoke, 166-hook rollout and ordinary publication proof'],
|
|
'support_hashes':{name:sha((OLD/name).read_bytes()) for name in ['activate_forge_template.py','verify_receive_native.py','activate_receive_hooks.py']},
|
|
'apps_preimage_hashes':{path:'sha256:'+sha((apps/path).read_bytes()) for path in ['helm/forgejo-values.yaml','manifests/forgejo-secret-scan-template.yaml']},
|
|
'cost_estimate_eur':0,'cost_note':'No additional infrastructure or paid service; human effort unpriced.',
|
|
'risk_note':'One existing Forgejo Recreate rollout and exact installed-hook copies; availability impact and partial rollout require guarded rollback. No credential, policy, image or database change.'
|
|
}
|
|
(proposal/'preparation.json').write_text(json.dumps(result,indent=2)+'\n')
|
|
print(json.dumps({k:result[k] for k in ['proposal_id','candidate_binary_sha256','new_configmap','conditional_forward_and_inverse_verified','deployment_server_dry_run_verified','configmap_server_dry_run_verified','native_changed','approval_requested']}))
|
|
|
|
if __name__=='__main__':
|
|
main()
|