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
« 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."""
3from __future__ import annotations
5import copy
6import json
7from typing import Any
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)
45def action_final_inventory(ctx: RunContext) -> dict[str, Any]:
46 """Prove cleanup and exact protected-stack/ECR and Transaction Search baseline preservation.
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")
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