mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 01:12:07 +00:00
test(terraform): assert dev import live baseline
This commit is contained in:
parent
b6d2beaa3b
commit
31b443e752
3 changed files with 233 additions and 3 deletions
|
|
@ -8,7 +8,11 @@ import json
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from terraform_import_plan_resources import REQUIRED_RESOURCES
|
from terraform_import_plan_resources import (
|
||||||
|
DEV_IMPORT_BASELINE,
|
||||||
|
DEV_IMPORT_IDS,
|
||||||
|
REQUIRED_RESOURCES,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
ALLOWED_MANAGED_TYPES = {
|
ALLOWED_MANAGED_TYPES = {
|
||||||
|
|
@ -38,9 +42,56 @@ def parse_args() -> argparse.Namespace:
|
||||||
"no-op import is proven. Repeat for each reviewed update."
|
"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()
|
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:
|
def main() -> int:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
plan = json.loads(args.plan_json.read_text(encoding="utf-8"))
|
plan = json.loads(args.plan_json.read_text(encoding="utf-8"))
|
||||||
|
|
@ -51,6 +102,8 @@ def main() -> int:
|
||||||
seen_update_addresses: set[str] = set()
|
seen_update_addresses: set[str] = set()
|
||||||
seen_addresses: set[str] = set()
|
seen_addresses: set[str] = set()
|
||||||
required_resources = REQUIRED_RESOURCES[args.environment]
|
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", []):
|
for resource in plan.get("resource_changes", []):
|
||||||
if resource.get("mode", "managed") != "managed":
|
if resource.get("mode", "managed") != "managed":
|
||||||
|
|
@ -61,6 +114,7 @@ def main() -> int:
|
||||||
actions = set(resource.get("change", {}).get("actions", []))
|
actions = set(resource.get("change", {}).get("actions", []))
|
||||||
managed += 1
|
managed += 1
|
||||||
seen_addresses.add(address)
|
seen_addresses.add(address)
|
||||||
|
resources_by_address[address] = resource
|
||||||
|
|
||||||
if resource_type not in ALLOWED_MANAGED_TYPES:
|
if resource_type not in ALLOWED_MANAGED_TYPES:
|
||||||
violations.append(
|
violations.append(
|
||||||
|
|
@ -89,12 +143,31 @@ def main() -> int:
|
||||||
f"{address}: update is not explicitly allowlisted"
|
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):
|
for unused in sorted(allowed_update_addresses - seen_update_addresses):
|
||||||
violations.append(f"{unused}: allowlisted update address is not updating")
|
violations.append(f"{unused}: allowlisted update address is not updating")
|
||||||
|
|
||||||
for missing in sorted(set(required_resources) - seen_addresses):
|
for missing in sorted(set(required_resources) - seen_addresses):
|
||||||
violations.append(f"{missing}: required managed resource is absent")
|
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:
|
if violations:
|
||||||
print("FAIL: live Terraform plan is not import-safe", file=sys.stderr)
|
print("FAIL: live Terraform plan is not import-safe", file=sys.stderr)
|
||||||
for violation in violations:
|
for violation in violations:
|
||||||
|
|
@ -102,6 +175,28 @@ def main() -> int:
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
mode = "controlled update" if allowed_update_addresses else "no-op import"
|
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(
|
print(
|
||||||
f"PASS: {mode} plan has {managed} managed resources, "
|
f"PASS: {mode} plan has {managed} managed resources, "
|
||||||
f"{updates} updates, and no create/delete/replace actions"
|
f"{updates} updates, and no create/delete/replace actions"
|
||||||
|
|
|
||||||
|
|
@ -30,3 +30,44 @@ REQUIRED_RESOURCES = {
|
||||||
"module.environment.aws_route53_record.api_cname[0]": "aws_route53_record",
|
"module.environment.aws_route53_record.api_cname[0]": "aws_route53_record",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEV_IMPORT_IDS = {
|
||||||
|
"module.environment.aws_elastic_beanstalk_environment.this": "e-hehnrqjjrt",
|
||||||
|
"module.environment.aws_iam_instance_profile.runtime": "shoc-backend-dev",
|
||||||
|
"module.environment.aws_iam_role.github_deploy": "githubdeploy-shoc-backend-dev",
|
||||||
|
"module.environment.aws_iam_role.runtime": "shoc-backend-dev",
|
||||||
|
"module.environment.aws_iam_role_policy.github_deploy": (
|
||||||
|
"githubdeploy-shoc-backend-dev:GithubDeployRoleDefaultPolicyE8F540D1"
|
||||||
|
),
|
||||||
|
"module.environment.aws_iam_role_policy.runtime_app_config": (
|
||||||
|
"shoc-backend-dev:shoc-dev-secrets-read"
|
||||||
|
),
|
||||||
|
"module.environment.aws_iam_role_policy.runtime_dynamo[0]": (
|
||||||
|
"shoc-backend-dev:shoc-assume-dynamo-reader"
|
||||||
|
),
|
||||||
|
"module.environment.aws_iam_role_policy.runtime_webhook[0]": (
|
||||||
|
"shoc-backend-dev:shoc-procurement-webhook-hmac-read"
|
||||||
|
),
|
||||||
|
"module.environment.aws_iam_role_policy_attachment.web_tier": (
|
||||||
|
"shoc-backend-dev/arn:aws:iam::aws:policy/AWSElasticBeanstalkWebTier"
|
||||||
|
),
|
||||||
|
"module.environment.aws_secretsmanager_secret.app_config": (
|
||||||
|
"arn:aws:secretsmanager:us-east-1:396287094661:secret:"
|
||||||
|
"shoc/dev/app-config-jLRBiw"
|
||||||
|
),
|
||||||
|
"module.environment.aws_route53_record.api_alias[0]": (
|
||||||
|
"Z07671212N75U4YLPWZR8_api.dev.seahaven.com_A"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
DEV_IMPORT_BASELINE = {
|
||||||
|
"environment_tags": {
|
||||||
|
"env": "dev",
|
||||||
|
"project": "shoc",
|
||||||
|
},
|
||||||
|
"api_alias": {
|
||||||
|
"name": "awseb--awseb-cmpb3ypfib53-1654918745.us-east-1.elb.amazonaws.com",
|
||||||
|
"zone_id": "Z35SXDOTRQ7X7K",
|
||||||
|
"evaluate_target_health": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@ import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from terraform_import_plan_resources import REQUIRED_RESOURCES
|
from terraform_import_plan_resources import (
|
||||||
|
DEV_IMPORT_BASELINE,
|
||||||
|
DEV_IMPORT_IDS,
|
||||||
|
REQUIRED_RESOURCES,
|
||||||
|
)
|
||||||
|
|
||||||
SCRIPT = Path(__file__).with_name("check-terraform-import-plan.py")
|
SCRIPT = Path(__file__).with_name("check-terraform-import-plan.py")
|
||||||
|
|
||||||
|
|
@ -21,6 +25,8 @@ def run_case(
|
||||||
omit_address: str | None = None,
|
omit_address: str | None = None,
|
||||||
extra_resource: tuple[str, str, list[str]] | None = None,
|
extra_resource: tuple[str, str, list[str]] | None = None,
|
||||||
allowed_updates: tuple[str, ...] = (),
|
allowed_updates: tuple[str, ...] = (),
|
||||||
|
import_id_overrides: dict[str, str] | None = None,
|
||||||
|
state_overrides: dict[str, dict[str, object]] | None = None,
|
||||||
empty: bool = False,
|
empty: bool = False,
|
||||||
) -> subprocess.CompletedProcess[str]:
|
) -> subprocess.CompletedProcess[str]:
|
||||||
changes = []
|
changes = []
|
||||||
|
|
@ -29,12 +35,39 @@ def run_case(
|
||||||
if address == omit_address:
|
if address == omit_address:
|
||||||
continue
|
continue
|
||||||
actions = (actions_by_address or {}).get(address, ["no-op"])
|
actions = (actions_by_address or {}).get(address, ["no-op"])
|
||||||
|
change: dict[str, object] = {"actions": actions}
|
||||||
|
if environment == "dev":
|
||||||
|
change["importing"] = {
|
||||||
|
"id": (import_id_overrides or {}).get(
|
||||||
|
address, DEV_IMPORT_IDS[address]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
address
|
||||||
|
== "module.environment.aws_elastic_beanstalk_environment.this"
|
||||||
|
):
|
||||||
|
state: dict[str, object] = {
|
||||||
|
"tags": DEV_IMPORT_BASELINE["environment_tags"],
|
||||||
|
"setting": [],
|
||||||
|
}
|
||||||
|
change["before"] = state
|
||||||
|
change["after"] = state
|
||||||
|
elif (
|
||||||
|
address
|
||||||
|
== "module.environment.aws_route53_record.api_alias[0]"
|
||||||
|
):
|
||||||
|
state = {"alias": [DEV_IMPORT_BASELINE["api_alias"]]}
|
||||||
|
change["before"] = state
|
||||||
|
change["after"] = state
|
||||||
|
if address in (state_overrides or {}):
|
||||||
|
change["before"] = (state_overrides or {})[address]
|
||||||
|
change["after"] = (state_overrides or {})[address]
|
||||||
changes.append(
|
changes.append(
|
||||||
{
|
{
|
||||||
"address": address,
|
"address": address,
|
||||||
"mode": "managed",
|
"mode": "managed",
|
||||||
"type": resource_type,
|
"type": resource_type,
|
||||||
"change": {"actions": actions},
|
"change": change,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if extra_resource:
|
if extra_resource:
|
||||||
|
|
@ -83,6 +116,67 @@ def main() -> int:
|
||||||
run_case("dev", actions_by_address={controlled_address: ["update"]}),
|
run_case("dev", actions_by_address={controlled_address: ["update"]}),
|
||||||
1,
|
1,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"unexpected initial action",
|
||||||
|
run_case("dev", actions_by_address={controlled_address: ["read"]}),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"wrong import id",
|
||||||
|
run_case(
|
||||||
|
"dev",
|
||||||
|
import_id_overrides={controlled_address: "wrong-role"},
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"wrong environment tags",
|
||||||
|
run_case(
|
||||||
|
"dev",
|
||||||
|
state_overrides={
|
||||||
|
"module.environment.aws_elastic_beanstalk_environment.this": {
|
||||||
|
"tags": {
|
||||||
|
"Name": "shoc-backend-dev",
|
||||||
|
"env": "dev",
|
||||||
|
"project": "shoc",
|
||||||
|
},
|
||||||
|
"setting": [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"managed settings during import",
|
||||||
|
run_case(
|
||||||
|
"dev",
|
||||||
|
state_overrides={
|
||||||
|
"module.environment.aws_elastic_beanstalk_environment.this": {
|
||||||
|
"tags": DEV_IMPORT_BASELINE["environment_tags"],
|
||||||
|
"setting": [{"name": "ASPNETCORE_ENVIRONMENT"}],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"wrong api alias",
|
||||||
|
run_case(
|
||||||
|
"dev",
|
||||||
|
state_overrides={
|
||||||
|
"module.environment.aws_route53_record.api_alias[0]": {
|
||||||
|
"alias": [
|
||||||
|
{
|
||||||
|
"name": "shoc-backend-dev.us-east-1.elasticbeanstalk.com",
|
||||||
|
"zone_id": "Z117KPS5GTRQ2G",
|
||||||
|
"evaluate_target_health": True,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"controlled update",
|
"controlled update",
|
||||||
run_case(
|
run_case(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue