diff --git a/docs/managed-update-recovery-implementation.md b/docs/managed-update-recovery-implementation.md index 7379ae86..dd55be16 100644 --- a/docs/managed-update-recovery-implementation.md +++ b/docs/managed-update-recovery-implementation.md @@ -449,3 +449,11 @@ The isolated manager remains stopped, PostgreSQL's original live identity and all recovery evidence remain intact. Keep the guest idle during serialized builds rather than rebooting and invalidating that identity. No Yaya application or catalog mutation has occurred, and full rollback/success remains pending. + +The `dc84a8b6` combined suite subsequently passed **2,025 tests**, with all 532 +inputs stable. Its matching executable attempt was deliberately stopped before +completion when source review found that post-target rollback journals correctly +retain `preserve_original: null`. Relay lineage now distinguishes that verified +post-target state from pre-target `preserve_original: false`, and rejects missing +or nonboolean startup markers and inconsistent pairs. The expanded 53 Python +cases pass; the 2,025-test receipt predates this final helper-only correction. diff --git a/scripts/indeehub-maintenance-controller.py b/scripts/indeehub-maintenance-controller.py index aa4d0d4c..eec1f838 100644 --- a/scripts/indeehub-maintenance-controller.py +++ b/scripts/indeehub-maintenance-controller.py @@ -104,7 +104,7 @@ def verify_relay_image_lineage(image, unit_sha256, records, installed=None): for member in relay: original=member.get('original',{});recovery=member.get('recovery_image') or {} if original.get('name')!='indeedhub-relay' or recovery.get('image','').removeprefix('sha256:')!=image:continue - require((record['schema']==1 and (member.get('preserve_original') is None or member.get('preserve_original') is False) or record['schema']==2 and member.get('preserve_original') is False) and recovery.get('operation_id')==record['id'] and recovery.get('source_container_id')==original.get('container_id'),'Relay recovery ownership changed') + require((record['schema']==1 and (member.get('preserve_original') is None or member.get('preserve_original') is False) or record['schema']==2 and (record.get('target_startup_began') is True and member.get('preserve_original') is None or record.get('target_startup_began') is False and member.get('preserve_original') is False)) and recovery.get('operation_id')==record['id'] and recovery.get('source_container_id')==original.get('container_id'),'Relay recovery ownership changed') require(hashlib.sha256(member['pinned_original_body'].encode()).hexdigest()==unit_sha256,'Relay recovery unit lineage changed') require(re.fullmatch('[0-9a-f]{64}',original.get('container_id','')) is not None,'Invalid relay source identity') if len(seen)==1:require(isinstance(installed,dict) and installed.get('schema')==1 and installed.get('name')=='indeedhub-relay' and installed.get('operation')==record['id'] and installed.get('body')==member['pinned_original_body'],'Relay installed recipe does not own recovery lineage') diff --git a/tests/regression/test_indeehub_maintenance_controller.py b/tests/regression/test_indeehub_maintenance_controller.py index 8f2ecbfb..9f3f91a0 100644 --- a/tests/regression/test_indeehub_maintenance_controller.py +++ b/tests/regression/test_indeehub_maintenance_controller.py @@ -534,7 +534,7 @@ console.log('process identity cases passed');''' def test_relay_image_requires_qualified_base_or_completed_owned_recovery_lineage(self): import copy,hashlib image='d'*64;body='[Container]\nImage='+image+'\n';before='[Container]\nImage='+module.LEGACY_RELAY_IMAGE+'\n' - record={'schema':2,'id':self.operation,'package':'indeedhub','phase':'Restored','cleanup_done':True,'members':[{'original':{'name':'indeedhub-relay','image':module.LEGACY_RELAY_IMAGE,'body':before,'container_id':'e'*64},'pinned_original_body':body,'preserve_original':False,'recovery_image':{'image':image,'operation_id':self.operation,'source_container_id':'e'*64}}]} + record={'schema':2,'target_startup_began':False,'id':self.operation,'package':'indeedhub','phase':'Restored','cleanup_done':True,'members':[{'original':{'name':'indeedhub-relay','image':module.LEGACY_RELAY_IMAGE,'body':before,'container_id':'e'*64},'pinned_original_body':body,'preserve_original':False,'recovery_image':{'image':image,'operation_id':self.operation,'source_container_id':'e'*64}}]} digest=hashlib.sha256(body.encode()).hexdigest() module.verify_relay_image_lineage(module.LEGACY_RELAY_IMAGE,'unused',[]) installed={'schema':1,'name':'indeedhub-relay','operation':self.operation,'body':body} @@ -543,6 +543,13 @@ console.log('process identity cases passed');''' with self.assertRaises(RuntimeError):module.verify_relay_image_lineage(image,digest,[record],missing) for bad in [[],[record,record]]: with self.assertRaises(RuntimeError):module.verify_relay_image_lineage(image,digest,bad,installed) + post_target=copy.deepcopy(record);post_target['target_startup_began']=True;post_target['members'][0]['preserve_original']=None + module.verify_relay_image_lineage(image,digest,[post_target],installed) + for started,preserved in ((True,False),(False,None),(0,False),(1,None),(None,False)): + bad=copy.deepcopy(record);bad['target_startup_began']=started;bad['members'][0]['preserve_original']=preserved + with self.assertRaises(RuntimeError):module.verify_relay_image_lineage(image,digest,[bad],installed) + missing=copy.deepcopy(record);missing.pop('target_startup_began') + with self.assertRaises(RuntimeError):module.verify_relay_image_lineage(image,digest,[missing],installed) legacy=copy.deepcopy(record);legacy['schema']=1 for malformed in (0,'false',[],{}): legacy['members'][0]['preserve_original']=malformed