Coverage for scripts / live_release_validation / actions / final_inventory.py: 100.00%

52 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-10-28 16:19 +0000

1"""final-inventory: verify zero residual resources and baseline preservation.""" 

2 

3from __future__ import annotations 

4 

5import copy 

6import json 

7from typing import Any 

8 

9from ..context import ( 

10 _topology_regions, 

11) 

12from ..inventory import ( 

13 capture_baseline, 

14 collect_project_resources, 

15 compare_baseline, 

16 project_resources_are_absent, 

17 summarize_project_resources, 

18) 

19from ..models import RunContext, utc_now 

20from ..ownership.dynamodb_streams import ( 

21 _strip_expired_table_streams, 

22) 

23from ..ownership.ecr import ( 

24 _strip_accepted_retained_ecr, 

25 _strip_baseline_ecr, 

26 _strip_expected_retained_ecr, 

27) 

28from ..ownership.efs_automatic_backups import ( 

29 _strip_accepted_efs_automatic_backup_recovery_points, 

30) 

31from ..ownership.kms import ( 

32 _strip_expected_pending_kms, 

33) 

34from ..ownership.stacks import ( 

35 _verify_target_stack_absence, 

36) 

37from ..ownership.transaction_search import ( 

38 _verify_transaction_search_restored, 

39) 

40from ..ownership.vpc_endpoints import ( 

41 _strip_deleted_vpc_endpoints, 

42) 

43 

44 

45def action_final_inventory(ctx: RunContext) -> dict[str, Any]: 

46 """Prove cleanup and exact protected-stack/ECR and Transaction Search baseline preservation. 

47 

48 Transaction Search is account-level state no stack owns, so it gets its 

49 own comparison: every regional Region's X-Ray destination, GCO resource 

50 policy, and span log groups must match the baseline again, except a span 

51 log group that appeared while Transaction Search was already enabled at 

52 baseline, which is reported as accepted. 

53 """ 

54 if ctx.checkpoint.baseline is None: 

55 raise RuntimeError("Final inventory cannot compare without a baseline") 

56 enabled_regions = ctx.checkpoint.state.get("enabled_regions") 

57 if not enabled_regions: 

58 raise RuntimeError("Checkpoint omitted enabled Regions") 

59 

60 stack_absence = _verify_target_stack_absence(ctx) 

61 final_baseline = capture_baseline( 

62 ctx.session, 

63 enabled_regions=enabled_regions, 

64 ecr_regions=ctx.checkpoint.baseline.get("ecr_regions") or _topology_regions(ctx), 

65 protected_stack_names=ctx.settings.protected_stack_names, 

66 ) 

67 comparison_baseline, accepted_retained_ecr = _strip_expected_retained_ecr( 

68 ctx, 

69 final_baseline, 

70 ) 

71 differences = compare_baseline(ctx.checkpoint.baseline, comparison_baseline) 

72 project_inventory = collect_project_resources( 

73 ctx.session, 

74 enabled_regions=enabled_regions, 

75 expected_account=ctx.settings.expected_account, 

76 project_name=ctx.config.project_name, 

77 seed_region=ctx.config.global_region, 

78 validation_run_id=ctx.settings.run_id, 

79 ) 

80 residual_inventory = _strip_baseline_ecr( 

81 project_inventory, 

82 ctx.checkpoint.baseline, 

83 ) 

84 residual_inventory = _strip_accepted_retained_ecr( 

85 residual_inventory, 

86 accepted_retained_ecr, 

87 ) 

88 residual_inventory, accepted_pending_kms = _strip_expected_pending_kms( 

89 ctx, 

90 residual_inventory, 

91 ) 

92 residual_inventory, accepted_efs_backups = _strip_accepted_efs_automatic_backup_recovery_points( 

93 ctx, residual_inventory 

94 ) 

95 residual_inventory, accepted_expired_streams = _strip_expired_table_streams( 

96 ctx, 

97 residual_inventory, 

98 ) 

99 residual_inventory, accepted_deleted_vpc_endpoints = _strip_deleted_vpc_endpoints( 

100 ctx, 

101 residual_inventory, 

102 ) 

103 transaction_search = _verify_transaction_search_restored(ctx) 

104 summary = summarize_project_resources(residual_inventory) 

105 result = { 

106 "summary": summary, 

107 "stack_absence": stack_absence, 

108 "baseline_differences": differences, 

109 "protected_and_ecr_inventory": final_baseline, 

110 "comparison_inventory": comparison_baseline, 

111 "accepted_retained_ecr": accepted_retained_ecr, 

112 "project_resources": project_inventory, 

113 "accepted_pending_kms_keys": accepted_pending_kms, 

114 "accepted_efs_automatic_backup_recovery_points": accepted_efs_backups, 

115 "accepted_expired_dynamodb_streams": accepted_expired_streams, 

116 "accepted_deleted_vpc_endpoints": accepted_deleted_vpc_endpoints, 

117 "residual_project_resources": residual_inventory, 

118 "transaction_search": transaction_search, 

119 } 

120 ctx.report.final_inventory = result 

121 ctx.checkpoint.state["final_inventory"] = copy.deepcopy(result) 

122 if not stack_absence["all_absent"] and ctx.checkpoint.destroyed: 

123 ctx.checkpoint.destroyed = False 

124 for action_name in ("destroy", "final-inventory"): 

125 if action_name in ctx.checkpoint.completed_actions: 

126 ctx.checkpoint.completed_actions.remove(action_name) 

127 ctx.checkpoint.state.setdefault("stale_destroyed_reconciliations", []).append( 

128 {"at": utc_now(), "stack_absence": stack_absence, "source": "final-inventory"} 

129 ) 

130 ctx.persist() 

131 if not stack_absence["all_absent"]: 

132 raise RuntimeError( 

133 "Target stacks remain after teardown: " 

134 + json.dumps(stack_absence["residual"], sort_keys=True) 

135 ) 

136 if differences: 

137 raise RuntimeError( 

138 "Protected stack/ECR baseline changed: " + json.dumps(differences, sort_keys=True) 

139 ) 

140 if transaction_search["differences"]: 

141 raise RuntimeError( 

142 "Transaction Search state differs from the baseline: " 

143 + "; ".join(transaction_search["differences"]) 

144 ) 

145 if not project_resources_are_absent(residual_inventory): 

146 raise RuntimeError( 

147 "Project resources remain after teardown: " 

148 + json.dumps(residual_inventory, sort_keys=True) 

149 ) 

150 return result