#!/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 ALLOWED_MANAGED_TYPES = {"aws_iam_role", "aws_iam_role_policy"} UNSAFE_ACTIONS = {"create", "delete"} def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("plan_json", type=Path) parser.add_argument( "--allow-update", action="store_true", help="Allow in-place updates after the initial no-op import is proven.", ) return parser.parse_args() def main() -> int: args = parse_args() plan = json.loads(args.plan_json.read_text(encoding="utf-8")) violations: list[str] = [] managed = 0 updates = 0 for resource in plan.get("resource_changes", []): if resource.get("mode", "managed") != "managed": continue resource_type = resource.get("type", "") address = resource.get("address", "") actions = set(resource.get("change", {}).get("actions", [])) managed += 1 if resource_type not in ALLOWED_MANAGED_TYPES: violations.append( f"{address}: managed type {resource_type!r} is outside the live ownership boundary" ) unsafe = sorted(actions & UNSAFE_ACTIONS) if unsafe: violations.append(f"{address}: unsafe actions {unsafe}") if "update" in actions: updates += 1 if not args.allow_update: violations.append( f"{address}: update is forbidden during the initial no-op import" ) 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 args.allow_update else "no-op import" 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())