diff --git a/diagnostic/build-3d7f3362.json b/diagnostic/build-3d7f3362.json new file mode 100644 index 00000000..601394d5 --- /dev/null +++ b/diagnostic/build-3d7f3362.json @@ -0,0 +1 @@ +{"commit": "3d7f3362", "generated_at": "2026-07-08T22:50:00Z", "validator": "test_terraform_validation.py"} diff --git a/diagnostic/build-3d7f3362.logd b/diagnostic/build-3d7f3362.logd new file mode 100644 index 00000000..37d5241b --- /dev/null +++ b/diagnostic/build-3d7f3362.logd @@ -0,0 +1,10 @@ +Tent of Trials - Build Summary +================================================== +generated_at: 2026-07-08T22:50:00.000000+00:00 +generator: test_terraform_validation.py +total_modules: 1 +passed: 1 +failed: 0 + +module results: + test_terraform_validation: PASS (4 tests: valid names, invalid names, mixed validation, CSV flow) diff --git a/tools/terraform_import.py b/tools/terraform_import.py index 9241ef31..9286b5aa 100644 --- a/tools/terraform_import.py +++ b/tools/terraform_import.py @@ -108,6 +108,26 @@ class ResourceToImport: import_status: str = "pending" error_message: str = "" + @staticmethod + def validate_resource_name(name: str) -> Tuple[bool, str]: + """Validate a Terraform resource name. + + Terraform resource names must be valid HCL identifiers: + - Start with a letter or underscore + - Contain only letters, digits, underscores + - Not be empty + + Returns (True, "") if valid, (False, error_message) if invalid. + """ + if not name or not name.strip(): + return False, "Resource name cannot be empty" + name = name.strip() + if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', name): + if '-' in name: + return False, f"Invalid resource name '{name}': hyphens are not allowed in Terraform resource names (use underscores instead)" + return False, f"Invalid resource name '{name}': must start with a letter or underscore and contain only letters, digits, and underscores" + return True, "" + @dataclass class ImportResult: success_count: int = 0 @@ -508,6 +528,19 @@ def main(): logger.info(f"Loaded {len(resources_to_import)} resources from {args.csv}") + # Validate all resource names before any operation + invalid = [] + for r in resources_to_import: + valid, msg = ResourceToImport.validate_resource_name(r.resource_name) + if not valid: + invalid.append((r.resource_type, r.resource_name, msg)) + + if invalid: + logger.error(f"{len(invalid)} resource(s) with invalid names:") + for rtype, rname, msg in invalid: + logger.error(f" {rtype}.{rname}: {msg}") + return 1 + if args.generate_script: importer.generate_import_script(resources_to_import, args.generate_script) else: diff --git a/tools/test_terraform_validation.py b/tools/test_terraform_validation.py new file mode 100644 index 00000000..b7fe172b --- /dev/null +++ b/tools/test_terraform_validation.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Test Terraform resource name validation in terraform_import.py""" + +import os +import sys +import tempfile +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from terraform_import import ResourceToImport, TerraformImporter + + +VALID_NAMES = [ + ("simple", "my_resource"), + ("with_numbers", "resource_123"), + ("starting_with_underscore", "_private_resource"), + ("single_letter", "a"), + ("mixed_case", "MyAWSInstance"), +] + +INVALID_NAMES = [ + ("with_hyphen", "my-resource", "hyphens are not allowed"), + ("starts_with_digit", "123resource", "start with a letter"), + ("empty_string", "", "cannot be empty"), + ("whitespace_only", " ", "cannot be empty"), + ("has_dot", "my.resource", "contain only letters"), + ("has_special_chars", "resource@name", "contain only letters"), +] + + +def test_valid_names(): + print(" Valid names:") + for label, name in VALID_NAMES: + valid, msg = ResourceToImport.validate_resource_name(name) + assert valid, f"Expected '{name}' to be valid, got: {msg}" + print(f" ✓ '{name}'") + print(f" → {len(VALID_NAMES)} valid names pass") + + +def test_invalid_names(): + print(" Invalid names:") + for label, name, expected_hint in INVALID_NAMES: + valid, msg = ResourceToImport.validate_resource_name(name) + assert not valid, f"Expected '{name}' to be invalid" + assert expected_hint in msg.lower(), f"Expected hint '{expected_hint}' in: {msg}" + print(f" ✓ '{name}' → {msg}") + print(f" → {len(INVALID_NAMES)} invalid names correctly rejected") + + +def test_validation_in_import_flow(): + """Test that invalid names cause early rejection in the import flow.""" + resources = [ + ResourceToImport( + resource_type="aws_instance", + resource_name="web-server", + resource_id="i-12345", + ), + ResourceToImport( + resource_type="aws_s3_bucket", + resource_name="valid_bucket", + resource_id="my-bucket", + ), + ] + + # Validate individually + valid1, msg1 = ResourceToImport.validate_resource_name(resources[0].resource_name) + assert not valid1, "web-server should be invalid" + valid2, msg2 = ResourceToImport.validate_resource_name(resources[1].resource_name) + assert valid2, "valid_bucket should be valid" + + print(" ✓ Mixed validation works: hyphenated rejected, valid accepted") + + +def test_validation_with_csv_flow(): + """Simulate CSV-driven import to verify names are checked before import.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as f: + f.write("type,name,id\n") + f.write("aws_instance,web-server,i-12345\n") + f.write("aws_s3_bucket,data_bucket,my-bucket\n") + csv_path = f.name + + try: + from terraform_import import parse_args + # Directly test the validation logic that runs before import + resources = [] + import csv + with open(csv_path, 'r') as f: + reader = csv.DictReader(f) + for row in reader: + resources.append(ResourceToImport( + resource_type=row.get("type", ""), + resource_name=row.get("name", ""), + resource_id=row.get("id", ""), + )) + + invalid = [] + for r in resources: + valid, msg = ResourceToImport.validate_resource_name(r.resource_name) + if not valid: + invalid.append((r.resource_type, r.resource_name, msg)) + + assert len(invalid) == 1, f"Expected 1 invalid name, got {len(invalid)}" + assert invalid[0][1] == "web-server", f"Expected web-server to be invalid" + print(f" ✓ CSV flow: 1 invalid (web-server), 1 valid (data_bucket)") + finally: + os.unlink(csv_path) + + +def main(): + print("=" * 60) + print("Terraform Resource Name Validation Tests") + print("=" * 60) + + tests = [ + test_valid_names, + test_invalid_names, + test_validation_in_import_flow, + test_validation_with_csv_flow, + ] + + failures = 0 + for test in tests: + print(f"\n {test.__name__}:") + try: + test() + except Exception as e: + print(f" ✗ FAILED: {e}") + failures += 1 + + print(f"\n {'=' * 50}") + total = len(tests) + passed = total - failures + print(f" Results: {total} tests, {passed} passed, {failures} failed") + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file