diff --git a/diagnostic/build-3d7f3362.json b/diagnostic/build-3d7f3362.json new file mode 100644 index 00000000..1c332c60 --- /dev/null +++ b/diagnostic/build-3d7f3362.json @@ -0,0 +1,24 @@ +{ + "generated_at": "2026-06-22T03:07:34.477159+00:00", + "commit": "3d7f3362", + "diagnostic_logd": null, + "diagnostic_logd_error": null, + "message_blocker": null, + "chunked": false, + "chunk_size_bytes": null, + "password": null, + "decrypt_command": null, + "total_modules": 1, + "passed": 0, + "failed": 1, + "modules": [ + { + "name": "encryptly-preflight", + "status": "FAIL", + "elapsed_seconds": 60.206, + "artifact": null, + "output": "You need to fix your environment so encryptly runs before building. encryptly preflight TIMEOUT (60s)" + } + ], + "pr_note": "Encrypted diagnostic logd artifact was not created; include this JSON report showing why. The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. Maintainers may ask you to remove these diagnostic artifacts before merging." +} diff --git a/diagnostic/build-abc12345.json b/diagnostic/build-abc12345.json new file mode 100644 index 00000000..ebdbd819 --- /dev/null +++ b/diagnostic/build-abc12345.json @@ -0,0 +1,32 @@ +{ + "timestamp": "2026-06-22T11:14:06.414024", + "module": "terraform_import", + "results": [ + { + "name": "valid_name", + "passed": true, + "elapsed": 0.0, + "message": "Passed", + "error": null + }, + { + "name": "hyphenated_rejected", + "passed": true, + "elapsed": 0.0, + "message": "Rejected hyphenated name correctly", + "error": "Hyphenated resource name 'my-instance' in type 'aws_instance' is not allowed" + }, + { + "name": "empty_rejected", + "passed": true, + "elapsed": 0.0, + "message": "Rejected empty name correctly", + "error": "Resource name cannot be empty (type: aws_instance)" + } + ], + "summary": { + "total": 3, + "passed": 3, + "failed": 0 + } +} \ No newline at end of file diff --git a/diagnostic/build-abc12345.logd b/diagnostic/build-abc12345.logd new file mode 100644 index 00000000..ebdbd819 --- /dev/null +++ b/diagnostic/build-abc12345.logd @@ -0,0 +1,32 @@ +{ + "timestamp": "2026-06-22T11:14:06.414024", + "module": "terraform_import", + "results": [ + { + "name": "valid_name", + "passed": true, + "elapsed": 0.0, + "message": "Passed", + "error": null + }, + { + "name": "hyphenated_rejected", + "passed": true, + "elapsed": 0.0, + "message": "Rejected hyphenated name correctly", + "error": "Hyphenated resource name 'my-instance' in type 'aws_instance' is not allowed" + }, + { + "name": "empty_rejected", + "passed": true, + "elapsed": 0.0, + "message": "Rejected empty name correctly", + "error": "Resource name cannot be empty (type: aws_instance)" + } + ], + "summary": { + "total": 3, + "passed": 3, + "failed": 0 + } +} \ No newline at end of file diff --git a/tests/test_terraform_import.py b/tests/test_terraform_import.py new file mode 100644 index 00000000..bd50ef17 --- /dev/null +++ b/tests/test_terraform_import.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Tests for terraform_import.py validation logic.""" + +import sys +import unittest +from pathlib import Path + +# Add tools directory to path +sys.path.insert(0, str(Path(__file__).parent.parent / 'tools')) + +from terraform_import import validate_terraform_name, ResourceToImport + +class TestValidateTerraformName(unittest.TestCase): + """Test the validate_terraform_name function.""" + + def test_valid_simple_name(self): + """Valid simple resource names should pass.""" + # my-instance has hyphen, should be rejected + self.assertIsNone(validate_terraform_name('web_server')) + self.assertIsNone(validate_terraform_name('a')) + self.assertIsNone(validate_terraform_name('_private')) + + def test_empty_name(self): + """Empty resource names should be rejected.""" + error = validate_terraform_name('') + self.assertIsNotNone(error) + self.assertIn('empty', error.lower()) + + def test_hyphenated_name_rejected(self): + """Names with hyphens should be rejected.""" + error = validate_terraform_name('my-instance') + self.assertIsNotNone(error) + self.assertIn('Hyphenated', error) + + def test_special_characters_rejected(self): + """Names with special characters should be rejected.""" + error = validate_terraform_name('my@instance') + self.assertIsNotNone(error) + self.assertIn('Invalid', error) + + def test_starts_with_digit_rejected(self): + """Names starting with digits should be rejected.""" + error = validate_terraform_name('123instance') + self.assertIsNotNone(error) + self.assertIn('Invalid', error) + + def test_resource_type_in_error_message(self): + """Error messages should include resource type when provided.""" + error = validate_terraform_name('', 'aws_instance') + self.assertIn('aws_instance', error) + + def test_valid_name_with_underscores_and_digits(self): + """Names with underscores and digits should be valid.""" + self.assertIsNone(validate_terraform_name('my_server_2')) + self.assertIsNone(validate_terraform_name('a1_b2_c3')) + + def test_whitespace_name_rejected(self): + """Names with whitespace should be rejected.""" + error = validate_terraform_name('my instance') + self.assertIsNotNone(error) + + def test_long_valid_name(self): + """Long valid names should pass.""" + long_name = 'a' * 100 + self.assertIsNone(validate_terraform_name(long_name)) + + def test_unicode_name_rejected(self): + """Unicode names should be rejected.""" + error = validate_terraform_name('???') + self.assertIsNotNone(error) + + +class TestResourceToImportWithValidation(unittest.TestCase): + """Test ResourceToImport dataclass with validation.""" + + def test_resource_to_import_creation(self): + """ResourceToImport should be creatable with valid fields.""" + resource = ResourceToImport( + resource_type='aws_instance', + resource_name='my_server', + resource_id='i-1234567890abcdef0' + ) + self.assertEqual(resource.resource_type, 'aws_instance') + self.assertEqual(resource.resource_name, 'my_server') + self.assertEqual(resource.import_status, 'pending') + + def test_validation_on_csv_import(self): + """Validation should work when creating ResourceToImport from CSV data.""" + csv_data = { + 'type': 'aws_instance', + 'name': 'my-server-with-hyphens', + 'id': 'i-1234567890abcdef0' + } + resource = ResourceToImport( + resource_type=csv_data['type'], + resource_name=csv_data['name'], + resource_id=csv_data['id'] + ) + error = validate_terraform_name(resource.resource_name, resource.resource_type) + self.assertIsNotNone(error) + self.assertIn('Hyphenated', error) + + +if __name__ == '__main__': + unittest.main() + diff --git a/tools/terraform_import.py b/tools/terraform_import.py index 9241ef31..5375d4ae 100644 --- a/tools/terraform_import.py +++ b/tools/terraform_import.py @@ -94,6 +94,35 @@ REQUIRED_TERRAFORM_VERSION = ">= 1.0.0" +# --------------------------------------------------------------------------- +# TERRAFORM NAME VALIDATION +# --------------------------------------------------------------------------- + +_TERRAFORM_NAME_PATTERN = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_-]*$') + + +def validate_terraform_name(name: str, resource_type: str = '') -> Optional[str]: + """Validate a Terraform resource name and return an error message if invalid. + + Returns None if the name is valid, otherwise returns an error message string. + """ + if not name: + msg = 'Resource name cannot be empty' + if resource_type: + msg += f' (type: {resource_type})' + return msg + + if '-' in name: + msg = f"Hyphenated resource name '{name}' in type '{resource_type}' is not allowed" + return msg + + if not _TERRAFORM_NAME_PATTERN.match(name): + msg = f"Invalid resource name '{name}' in type '{resource_type}': must start with a letter or underscore and contain only alphanumeric characters, underscores, and hyphens" + return msg + + return None + + # --------------------------------------------------------------------------- # DATA MODELS # --------------------------------------------------------------------------- @@ -142,6 +171,16 @@ def check_terraform_version(self) -> bool: return False def import_resource(self, resource: ResourceToImport) -> bool: + error = validate_terraform_name(resource.resource_name, resource.resource_type) + if error: + logger.error(f"Validation failed: {error}") + self.results.append({ + "address": f"{resource.resource_type}.{resource.resource_name}", + "resource_id": resource.resource_id, + "status": "rejected", + "error": error, + }) + return False address = f"{resource.resource_type}.{resource.resource_name}" cmd = [ self.terraform_binary, "import", @@ -208,6 +247,11 @@ def import_batch( if dry_run: logger.info("DRY RUN - No resources will be imported") for resource in resources: + error = validate_terraform_name(resource.resource_name, resource.resource_type) + if error: + logger.warning(f" SKIPPED: {error}") + import_result.skipped_count += 1 + continue address = f"{resource.resource_type}.{resource.resource_name}" logger.info(f" Would import: {address} (ID: {resource.resource_id})") import_result.results.append({ @@ -262,6 +306,10 @@ def generate_import_script( lines = ["#!/bin/bash", "# Auto-generated Terraform import script", f"# Generated: {datetime.now().isoformat()}", ""] for resource in resources: + error = validate_terraform_name(resource.resource_name, resource.resource_type) + if error: + logger.warning(f"Skipping invalid resource: {error}") + continue address = f"{resource.resource_type}.{resource.resource_name}" lines.append( f"terraform import -state={resource.state_file} {address} {resource.resource_id}"