mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 13:03:12 +00:00
208 lines
7 KiB
Python
208 lines
7 KiB
Python
#!/usr/bin/env python3
|
|
"""Reject unsafe actions in a live Terraform import plan."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from terraform_import_plan_resources import (
|
|
DEV_IMPORT_BASELINE,
|
|
DEV_IMPORT_IDS,
|
|
REQUIRED_RESOURCES,
|
|
)
|
|
|
|
|
|
ALLOWED_MANAGED_TYPES = {
|
|
resource_type
|
|
for resources in REQUIRED_RESOURCES.values()
|
|
for resource_type in resources.values()
|
|
}
|
|
UNSAFE_ACTIONS = {"create", "delete"}
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("plan_json", type=Path)
|
|
parser.add_argument(
|
|
"--environment",
|
|
required=True,
|
|
choices=sorted(REQUIRED_RESOURCES),
|
|
help="Exact environment ownership boundary expected in the plan.",
|
|
)
|
|
parser.add_argument(
|
|
"--allow-update-address",
|
|
action="append",
|
|
default=[],
|
|
metavar="ADDRESS",
|
|
help=(
|
|
"Allow an in-place update to this exact address after the initial "
|
|
"no-op import is proven. Repeat for each reviewed update."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--evidence-out",
|
|
type=Path,
|
|
help="Write machine-readable proof after every import assertion passes.",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def validate_dev_import_baseline(
|
|
resources_by_address: dict[str, dict], violations: list[str]
|
|
) -> None:
|
|
environment_address = (
|
|
"module.environment.aws_elastic_beanstalk_environment.this"
|
|
)
|
|
route_address = "module.environment.aws_route53_record.api_alias[0]"
|
|
expected_tags = DEV_IMPORT_BASELINE["environment_tags"]
|
|
expected_alias = DEV_IMPORT_BASELINE["api_alias"]
|
|
|
|
for side in ("before", "after"):
|
|
environment = (
|
|
resources_by_address.get(environment_address, {})
|
|
.get("change", {})
|
|
.get(side)
|
|
or {}
|
|
)
|
|
if environment.get("tags") != expected_tags:
|
|
violations.append(
|
|
f"{environment_address}: {side} environment tags do not match "
|
|
f"the exact dev import baseline"
|
|
)
|
|
if environment.get("setting") != []:
|
|
violations.append(
|
|
f"{environment_address}: {side} contains managed EB settings "
|
|
"during the import-only phase"
|
|
)
|
|
|
|
route = (
|
|
resources_by_address.get(route_address, {})
|
|
.get("change", {})
|
|
.get(side)
|
|
or {}
|
|
)
|
|
aliases = route.get("alias") or []
|
|
if len(aliases) != 1 or aliases[0] != expected_alias:
|
|
violations.append(
|
|
f"{route_address}: {side} alias does not match the exact "
|
|
"live ALB target and zone"
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
plan = json.loads(args.plan_json.read_text(encoding="utf-8"))
|
|
violations: list[str] = []
|
|
managed = 0
|
|
updates = 0
|
|
allowed_update_addresses = set(args.allow_update_address)
|
|
seen_update_addresses: set[str] = set()
|
|
seen_addresses: set[str] = set()
|
|
required_resources = REQUIRED_RESOURCES[args.environment]
|
|
resources_by_address: dict[str, dict] = {}
|
|
initial_import = not allowed_update_addresses
|
|
|
|
for resource in plan.get("resource_changes", []):
|
|
if resource.get("mode", "managed") != "managed":
|
|
continue
|
|
|
|
resource_type = resource.get("type", "")
|
|
address = resource.get("address", "<unknown>")
|
|
actions = set(resource.get("change", {}).get("actions", []))
|
|
managed += 1
|
|
seen_addresses.add(address)
|
|
resources_by_address[address] = resource
|
|
|
|
if resource_type not in ALLOWED_MANAGED_TYPES:
|
|
violations.append(
|
|
f"{address}: managed type {resource_type!r} is outside the live ownership boundary"
|
|
)
|
|
|
|
expected_type = required_resources.get(address)
|
|
if expected_type is None:
|
|
violations.append(
|
|
f"{address}: managed address is outside the live ownership boundary"
|
|
)
|
|
elif resource_type != expected_type:
|
|
violations.append(
|
|
f"{address}: expected managed type {expected_type!r}, got {resource_type!r}"
|
|
)
|
|
|
|
unsafe = sorted(actions & UNSAFE_ACTIONS)
|
|
if unsafe:
|
|
violations.append(f"{address}: unsafe actions {unsafe}")
|
|
|
|
if "update" in actions:
|
|
updates += 1
|
|
seen_update_addresses.add(address)
|
|
if address not in allowed_update_addresses:
|
|
violations.append(
|
|
f"{address}: update is not explicitly allowlisted"
|
|
)
|
|
|
|
if initial_import and args.environment == "dev":
|
|
if actions != {"no-op"}:
|
|
violations.append(
|
|
f"{address}: initial dev import actions must be ['no-op'], "
|
|
f"got {sorted(actions)}"
|
|
)
|
|
expected_import_id = DEV_IMPORT_IDS.get(address)
|
|
actual_import_id = (
|
|
resource.get("change", {}).get("importing") or {}
|
|
).get("id")
|
|
if actual_import_id != expected_import_id:
|
|
violations.append(
|
|
f"{address}: expected import id {expected_import_id!r}, "
|
|
f"got {actual_import_id!r}"
|
|
)
|
|
|
|
for unused in sorted(allowed_update_addresses - seen_update_addresses):
|
|
violations.append(f"{unused}: allowlisted update address is not updating")
|
|
|
|
for missing in sorted(set(required_resources) - seen_addresses):
|
|
violations.append(f"{missing}: required managed resource is absent")
|
|
|
|
if initial_import and args.environment == "dev":
|
|
validate_dev_import_baseline(resources_by_address, violations)
|
|
|
|
if violations:
|
|
print("FAIL: live Terraform plan is not import-safe", file=sys.stderr)
|
|
for violation in violations:
|
|
print(f" - {violation}", file=sys.stderr)
|
|
return 1
|
|
|
|
mode = "controlled update" if allowed_update_addresses else "no-op import"
|
|
if args.evidence_out:
|
|
evidence = {
|
|
"environment": args.environment,
|
|
"mode": mode,
|
|
"managed_resources": managed,
|
|
"updates": updates,
|
|
"creates": 0,
|
|
"deletes": 0,
|
|
"replacements": 0,
|
|
"imports": {
|
|
address: (
|
|
resource.get("change", {}).get("importing") or {}
|
|
).get("id")
|
|
for address, resource in sorted(resources_by_address.items())
|
|
},
|
|
}
|
|
if args.environment == "dev" and initial_import:
|
|
evidence["asserted_live_baseline"] = DEV_IMPORT_BASELINE
|
|
args.evidence_out.write_text(
|
|
json.dumps(evidence, indent=2, sort_keys=True) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
print(
|
|
f"PASS: {mode} plan has {managed} managed resources, "
|
|
f"{updates} updates, and no create/delete/replace actions"
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|