""" Post-Processor: Split LLM-generated HCL into complete Terraform module ===================================================================== Takes raw HCL from LLM → splits into main.tf, variables.tf, outputs.tf, README.md Features: - Extract hardcoded values → variables.tf with defaults - Auto-detect outputs from resource blocks - Generate README with usage instructions - terraform fmt compatible output """ import re from typing import Optional def extract_hcl_code(raw_output: str) -> str: """Extract HCL code from LLM output (strip markdown fences, explanations).""" # Try to find code in ```hcl or ``` blocks patterns = [ r'```(?:hcl|terraform)\s*\n(.*?)```', r'```\s*\n(.*?)```', ] for pat in patterns: matches = re.findall(pat, raw_output, re.DOTALL) if matches: return '\n\n'.join(matches).strip() # If no fences, try to find resource/data/locals blocks lines = raw_output.strip().split('\n') code_lines = [] in_code = False brace_depth = 0 for line in lines: stripped = line.strip() # Start of terraform block if re.match(r'^(resource|data|locals|variable|output|module|provider|terraform)\s+', stripped): in_code = True if in_code: code_lines.append(line) brace_depth += line.count('{') - line.count('}') if brace_depth <= 0 and len(code_lines) > 1: in_code = False brace_depth = 0 code_lines.append('') elif stripped == '' and code_lines: pass # skip blank lines between explanations result = '\n'.join(code_lines).strip() return result if result else raw_output.strip() def parse_resources(hcl_code: str) -> list[dict]: """Parse resource blocks from HCL code.""" resources = [] # Match resource "type" "name" { ... } pattern = r'resource\s+"([^"]+)"\s+"([^"]+)"' for match in re.finditer(pattern, hcl_code): resources.append({ 'type': match.group(1), 'name': match.group(2), 'full_ref': f'{match.group(1)}.{match.group(2)}' }) return resources def generate_variables(hcl_code: str, resources: list[dict]) -> str: """Generate variables.tf from detected patterns in main.tf.""" variables = [] # Common variable patterns to extract var_patterns = [ { 'pattern': r'region\s*=\s*"([\w-]+)"', 'name': 'aws_region', 'description': 'AWS region for resources', 'type': 'string', }, { 'pattern': r'cidr_block\s*=\s*"([\d./]+)"', 'name': 'vpc_cidr', 'description': 'CIDR block for the VPC', 'type': 'string', 'match_first': True, }, { 'pattern': r'instance_type\s*=\s*"([^"]+)"', 'name': 'instance_type', 'description': 'EC2 instance type', 'type': 'string', }, { 'pattern': r'engine_version\s*=\s*"([^"]+)"', 'name': 'engine_version', 'description': 'Database engine version', 'type': 'string', }, { 'pattern': r'allocated_storage\s*=\s*(\d+)', 'name': 'allocated_storage', 'description': 'Allocated storage in GB', 'type': 'number', }, ] seen = set() for vp in var_patterns: matches = re.findall(vp['pattern'], hcl_code) if matches and vp['name'] not in seen: default_val = matches[0] seen.add(vp['name']) if vp['type'] == 'number': default_line = f' default = {default_val}' else: default_line = f' default = "{default_val}"' variables.append(f'''variable "{vp['name']}" {{ description = "{vp['description']}" type = {vp['type']} {default_line} }}''') # Always add common variables if 'environment' not in seen: variables.insert(0, '''variable "environment" { description = "Environment name (e.g., dev, staging, prod)" type = string default = "dev" }''') if 'project_name' not in seen: variables.insert(0, '''variable "project_name" { description = "Project name used for resource naming and tagging" type = string default = "my-project" }''') if not variables: variables.append('# No additional variables detected') header = '''# ============================================== # Variables # ============================================== # Customize these variables for your environment # ============================================== ''' return header + '\n\n'.join(variables) + '\n' def generate_outputs(resources: list[dict], hcl_code: str) -> str: """Generate outputs.tf from detected resources.""" outputs = [] # Common output patterns per resource type output_map = { 'aws_vpc': [('vpc_id', 'id', 'The ID of the VPC')], 'aws_subnet': [('subnet_ids', 'id', 'The IDs of the subnets')], 'aws_instance': [('instance_id', 'id', 'The ID of the EC2 instance'), ('instance_public_ip', 'public_ip', 'Public IP of the instance')], 'aws_s3_bucket': [('bucket_name', 'id', 'The name of the S3 bucket'), ('bucket_arn', 'arn', 'The ARN of the S3 bucket')], 'aws_db_instance': [('db_endpoint', 'endpoint', 'The RDS instance endpoint'), ('db_name', 'db_name', 'The database name')], 'aws_lambda_function': [('lambda_arn', 'arn', 'The ARN of the Lambda function'), ('lambda_function_name', 'function_name', 'The Lambda function name')], 'aws_lb': [('lb_dns_name', 'dns_name', 'The DNS name of the load balancer'), ('lb_arn', 'arn', 'The ARN of the load balancer')], 'aws_ecs_cluster': [('ecs_cluster_name', 'name', 'The ECS cluster name'), ('ecs_cluster_arn', 'arn', 'The ECS cluster ARN')], 'aws_eks_cluster': [('eks_cluster_endpoint', 'endpoint', 'The EKS cluster endpoint'), ('eks_cluster_name', 'name', 'The EKS cluster name')], 'aws_api_gateway_rest_api': [('api_gateway_id', 'id', 'The API Gateway REST API ID')], 'aws_dynamodb_table': [('dynamodb_table_name', 'name', 'The DynamoDB table name'), ('dynamodb_table_arn', 'arn', 'The DynamoDB table ARN')], 'aws_cloudfront_distribution': [('cloudfront_domain_name', 'domain_name', 'The CloudFront distribution domain name')], 'aws_sqs_queue': [('sqs_queue_url', 'url', 'The SQS queue URL'), ('sqs_queue_arn', 'arn', 'The SQS queue ARN')], 'aws_sns_topic': [('sns_topic_arn', 'arn', 'The SNS topic ARN')], 'aws_iam_role': [('iam_role_arn', 'arn', 'The IAM role ARN')], 'aws_security_group': [('security_group_id', 'id', 'The security group ID')], 'aws_elasticache_replication_group': [('redis_endpoint', 'primary_endpoint_address', 'The Redis primary endpoint')], 'aws_efs_file_system': [('efs_id', 'id', 'The EFS file system ID')], 'aws_kinesis_stream': [('kinesis_stream_arn', 'arn', 'The Kinesis stream ARN')], 'aws_kms_key': [('kms_key_arn', 'arn', 'The KMS key ARN'), ('kms_key_id', 'key_id', 'The KMS key ID')], 'aws_route53_zone': [('route53_zone_id', 'zone_id', 'The Route 53 hosted zone ID')], 'aws_codepipeline': [('pipeline_arn', 'arn', 'The CodePipeline ARN')], 'aws_sfn_state_machine': [('state_machine_arn', 'arn', 'The Step Functions state machine ARN')], 'aws_ecr_repository': [('ecr_repository_url', 'repository_url', 'The ECR repository URL')], 'aws_cloudtrail': [('cloudtrail_arn', 'arn', 'The CloudTrail ARN')], 'aws_autoscaling_group': [('asg_name', 'name', 'The Auto Scaling Group name')], } seen_outputs = set() for res in resources: if res['type'] in output_map: for out_name, attr, desc in output_map[res['type']]: if out_name not in seen_outputs: seen_outputs.add(out_name) outputs.append(f'''output "{out_name}" {{ description = "{desc}" value = {res['full_ref']}.{attr} }}''') if not outputs: # Fallback: output first resource's id if resources: r = resources[0] outputs.append(f'''output "{r['type'].replace('aws_', '')}_id" {{ description = "The ID of {r['full_ref']}" value = {r['full_ref']}.id }}''') header = '''# ============================================== # Outputs # ============================================== # Useful values exported from this module # ============================================== ''' return header + '\n\n'.join(outputs) + '\n' def generate_readme(prompt: str, resources: list[dict], blueprint_name: str = None) -> str: """Generate README.md for the Terraform module.""" title = blueprint_name or "Terraform Module" resource_list = '\n'.join(f'- `{r["type"]}` — `{r["name"]}`' for r in resources) return f'''# {title} ## Description {prompt} ## Resources Created {resource_list} ## Usage ```hcl module "this" {{ source = "./module" project_name = "my-project" environment = "dev" }} ``` ## Requirements | Name | Version | |------|---------| | terraform | >= 1.0 | | aws | ~> 5.0 | ## Quick Start ```bash # Initialize Terraform terraform init # Preview changes terraform plan # Apply infrastructure terraform apply # Destroy when done terraform destroy ``` ## Generated by XAI-Enhanced Terraform Code Generator — Explainable AI techniques for LLM-based Infrastructure as Code generation. ''' def process_output(raw_output: str, original_prompt: str, blueprint_name: str = None) -> dict: """ Main entry point: process raw LLM output into complete Terraform module. Returns dict with keys: main_tf, variables_tf, outputs_tf, readme_md, resources """ # Step 1: Extract clean HCL main_tf = extract_hcl_code(raw_output) # Step 2: Remove provider/terraform blocks (pipeline already instructs this) main_tf = re.sub( r'(?:provider|terraform)\s+"?\w+"?\s*\{[^}]*(?:\{[^}]*\}[^}]*)*\}\s*\n?', '', main_tf ).strip() # Step 3: Parse resources resources = parse_resources(main_tf) # Step 4: Generate supporting files variables_tf = generate_variables(main_tf, resources) outputs_tf = generate_outputs(resources, main_tf) readme_md = generate_readme(original_prompt, resources, blueprint_name) # Add header comment to main.tf main_tf_header = '''# ============================================== # Main Terraform Configuration # ============================================== # Generated by XAI-Enhanced Terraform Generator # ============================================== ''' main_tf = main_tf_header + main_tf + '\n' return { 'main_tf': main_tf, 'variables_tf': variables_tf, 'outputs_tf': outputs_tf, 'readme_md': readme_md, 'resources': [r['full_ref'] for r in resources], 'resource_count': len(resources), } if __name__ == '__main__': # Test test_hcl = ''' resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true tags = { Name = "main-vpc" } } resource "aws_subnet" "public" { vpc_id = aws_vpc.main.id cidr_block = "10.0.1.0/24" availability_zone = "us-east-1a" tags = { Name = "public-subnet" } } resource "aws_internet_gateway" "main" { vpc_id = aws_vpc.main.id } ''' result = process_output(test_hcl, "Create a VPC with public subnet", "VPC Module") for key in ['main_tf', 'variables_tf', 'outputs_tf', 'readme_md']: print(f"\n{'='*50}") print(f" {key}") print(f"{'='*50}") print(result[key][:500]) print(f"\nResources: {result['resources']}")