DEV Community

Alain Airom (Ayrom)
Alain Airom (Ayrom)

Posted on

No More Automation from Scratch: Automate and Build Your Terraform with Draw.io Diagrams

Creating Terraform Scripts from Draw.io XML Files!

Introduction

The concept and the idea originated from a realization that drawings from open-source tools, such as Draw.io, are essentially structured XML files. If the diagram explicitly maps out an infrastructure architecture, it would be logical to analyze that XML file and programmatically generate corresponding Terraform scripts.

> Disclaimer: note that this application is currently in its alpha stage; as an early-access release, it requires more rigorous, “bulletproof” testing and further functional enhancements before it reaches full maturity. This tool marks the first in a planned series of automation utilities I am developing to significantly accelerate my personal productivity and streamline technical workflows.


A word on the external tool used: Draw.io

The core of this application’s utility is its deep integration with Draw.io, a premier open-source diagramming tool. Rather than forcing users into a proprietary design environment, the generator leverages Draw.io’s flexible, web-based interface and its extensive library of cloud architecture shapes. By treating the exported Draw.io XML as a structured data source, the application can precisely map visual components and their interconnections to valid Terraform resource blocks. This heavy reliance on an open-source standard ensures that users can design their infrastructure using a familiar, free, and highly accessible tool, while the backend handles the complex translation from a visual drawing to production-ready code.


Terraform Script Generator: A Diagram-to-Code Pipeline

The Terraform Script Generator is a web-based utility designed to streamline the translation of cloud infrastructure diagrams, specifically those created in Draw.io, into deployable Terraform configurations. This automation bridges the gap between architectural planning and infrastructure implementation, particularly for Cloud platforms like IBM (but not only), but adaptable to other platforms as well. The generator employs a client-server architecture, utilizing a user-friendly frontend to receive design inputs and a robust backend to handle the analytical heavy lifting.

terraform-builder/
│
├── backend/                      # Backend Python modules
│   ├── app.py                   # Flask API server
│   ├── drawio_parser.py         # XML parser for Draw.io
│   └── terraform_generator.py   # Terraform code generator
│
├── frontend/                     # Frontend web files
│   ├── index.html               # Main UI
│   ├── styles.css               # Styling
│   └── app.js                   # Client-side logic
│
├── k8s/                         # Kubernetes deployment files
│   ├── deployment.yaml          # Deployment and Service manifests
│   ├── configmap.yaml           # Application configuration
│   └── README.md                # Kubernetes deployment guide
│
├── Docs/                        # Documentation
│   └── Architecture.md          # This file
│
├── scripts/                     # Utility scripts
│   ├── start.sh                # Start application
│   ├── stop.sh                 # Stop application
│   └── validate_terraform.py   # Terraform validation
│
├── _diagrams/                   # Example diagrams
│   └── d1.drawio               # Sample IBM Cloud diagram
│
├── _images/                     # Project images
├── _sources/                    # Reference sources
│
├── Dockerfile                   # Docker image definition
├── requirements.txt             # Python dependencies
├── .gitignore                  # Git ignore rules
└── README.md                   # Project documentation
Enter fullscreen mode Exit fullscreen mode

The Backend: Analytical Engine with Flask and Python

At the heart of the system is a Python backend built with the Flask web framework. This application is responsible for orchestrating the overall process, from serving the main user interface to exposing REST API endpoints for diagram parsing and code generation. It integrates two key internal modules: DrawioParser and TerraformGenerator. The application logic is designed for extensibility, capable of handling complex diagrams and multi-stage processing, supporting features like file uploading, health checks, and state management via JSON for multi-step workflows.

# app.py - Main Flask application for the Terraform Builder
"""
Flask Backend for Terraform Builder Application
Provides API endpoints for parsing Draw.io diagrams and generating Terraform code
"""
from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS
import os
from drawio_parser import DrawioParser
from terraform_generator import TerraformGenerator

app = Flask(__name__, static_folder='../frontend', static_url_path='')
CORS(app)

# Initialize parsers and generators
parser = DrawioParser()
generator = TerraformGenerator()


@app.route('/')
def index():
    """Serve the main application page"""
    return send_from_directory(app.static_folder, 'index.html')


@app.route('/api/parse', methods=['POST'])
def parse_diagram():
    """Parse a Draw.io diagram and return components"""
    try:
        if 'file' in request.files:
            # Handle file upload
            file = request.files['file']
            if file.filename == '':
                return jsonify({'error': 'No file selected'}), 400

            # Save temporarily and parse
            temp_path = '/tmp/temp_diagram.drawio'
            file.save(temp_path)

            result = parser.parse_file(temp_path)
            os.remove(temp_path)

        elif 'content' in request.json:
            # Handle XML content
            content = request.json['content']
            result = parser.parse_content(content)
        else:
            return jsonify({'error': 'No diagram data provided'}), 400

        return jsonify({
            'success': True,
            'components': result['components'],
            'connections': result['connections'],
            'summary': parser.get_resource_summary()
        })

    except Exception as e:
        return jsonify({'error': str(e)}), 500


@app.route('/api/generate', methods=['POST'])
def generate_terraform():
    """Generate Terraform code from parsed components"""
    try:
        data = request.json
        components = data.get('components', [])
        provider = data.get('provider', 'ibm')

        if not components:
            return jsonify({'error': 'No components provided'}), 400

        # Create generator with selected provider
        gen = TerraformGenerator(provider=provider)

        # Generate Terraform files
        terraform_files = gen.generate_from_components(components)

        return jsonify({
            'success': True,
            'files': terraform_files
        })

    except Exception as e:
        return jsonify({'error': str(e)}), 500


@app.route('/api/parse-and-generate', methods=['POST'])
def parse_and_generate():
    """Parse diagram and generate Terraform code in one step"""
    try:
        provider = 'ibm'  # Default provider
        custom_provider_url = None

        if 'file' in request.files:
            # Handle file upload
            file = request.files['file']
            if file.filename == '':
                return jsonify({'error': 'No file selected'}), 400

            # Save temporarily and parse
            temp_path = '/tmp/temp_diagram.drawio'
            file.save(temp_path)

            result = parser.parse_file(temp_path)
            os.remove(temp_path)

            # Get provider from form data if available
            provider = request.form.get('provider', 'ibm')
            custom_provider_url = request.form.get('custom_provider_url')

        elif 'content' in request.json:
            # Handle XML content
            content = request.json['content']
            provider = request.json.get('provider', 'ibm')
            custom_provider_url = request.json.get('custom_provider_url')
            result = parser.parse_content(content)
        else:
            return jsonify({'error': 'No diagram data provided'}), 400

        # Create generator with selected provider and custom URL if provided
        gen = TerraformGenerator(provider=provider, custom_provider_url=custom_provider_url)

        # Generate Terraform files
        terraform_files = gen.generate_from_components(result['components'])

        response_data = {
            'success': True,
            'components': result['components'],
            'connections': result['connections'],
            'summary': parser.get_resource_summary(),
            'terraform': terraform_files,
            'provider': provider
        }

        # Add custom provider info if used
        if custom_provider_url and gen.custom_provider_config:
            response_data['custom_provider'] = gen.custom_provider_config

        # Add warning if IBM components detected with non-IBM provider
        if provider != 'ibm' and hasattr(gen, 'skipped_resources') and gen.skipped_resources:
            response_data['warning'] = {
                'message': f'This diagram contains {len(gen.skipped_resources)} IBM Cloud-specific component(s) that were not generated for the {provider.upper()} provider.',
                'suggestion': 'Consider selecting "IBM Cloud" as the provider to generate these resources.',
                'skipped_count': len(gen.skipped_resources)
            }

        return jsonify(response_data)

    except Exception as e:
        return jsonify({'error': str(e)}), 500


@app.route('/api/health', methods=['GET'])
def health_check():
    """Health check endpoint"""
    return jsonify({
        'status': 'healthy',
        'service': 'terraform-builder-api'
    })


@app.route('/api/diagram-preview', methods=['POST'])
def diagram_preview():
    """Generate a preview URL for the diagram"""
    try:
        data = request.json
        content = data.get('content', '')

        if not content:
            return jsonify({'error': 'No diagram content provided'}), 400

        # Return the content for client-side rendering
        return jsonify({
            'success': True,
            'content': content,
            'preview_available': True
        })

    except Exception as e:
        return jsonify({'error': str(e)}), 500


@app.route('/api/save-files', methods=['POST'])
def save_files():
    """Save generated Terraform files to timestamped output folder"""
    try:
        from datetime import datetime
        import shutil

        data = request.json
        files = data.get('files', {})
        diagram_name = data.get('diagram_name', 'diagram')

        if not files:
            return jsonify({'error': 'No files provided'}), 400

        # Remove file extension from diagram name
        diagram_name = diagram_name.replace('.drawio', '').replace('.xml', '')

        # Create timestamp
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')

        # Create output directory structure
        output_base = 'output'
        output_dir = os.path.join(output_base, f"{diagram_name}_{timestamp}")

        # Create directory if it doesn't exist
        os.makedirs(output_dir, exist_ok=True)

        # Save each file
        saved_files = []
        for filename, content in files.items():
            file_path = os.path.join(output_dir, filename)
            with open(file_path, 'w') as f:
                f.write(content)
            saved_files.append(file_path)

        return jsonify({
            'success': True,
            'output_dir': output_dir,
            'files': saved_files,
            'message': f'Files saved to {output_dir}'
        })

    except Exception as e:
        return jsonify({'error': str(e)}), 500


@app.route('/api/save-and-zip-files', methods=['POST'])
def save_and_zip_files():
    """Save generated Terraform files to timestamped output folder and return as ZIP"""
    try:
        from datetime import datetime
        import shutil
        import zipfile
        from io import BytesIO
        from flask import send_file

        data = request.json
        files = data.get('files', {})
        diagram_name = data.get('diagram_name', 'diagram')

        if not files:
            return jsonify({'error': 'No files provided'}), 400

        # Remove file extension from diagram name
        diagram_name = diagram_name.replace('.drawio', '').replace('.xml', '')

        # Create timestamp
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        folder_name = f"{diagram_name}_{timestamp}"

        # Create output directory structure
        output_base = 'output'
        output_dir = os.path.join(output_base, folder_name)

        # Create directory if it doesn't exist
        os.makedirs(output_dir, exist_ok=True)

        # Save each file to disk
        for filename, content in files.items():
            file_path = os.path.join(output_dir, filename)
            with open(file_path, 'w') as f:
                f.write(content)

        # Create ZIP file in memory
        zip_buffer = BytesIO()
        with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
            for filename, content in files.items():
                # Add files to ZIP with folder structure
                zip_file.writestr(f"{folder_name}/{filename}", content)

        zip_buffer.seek(0)
        zip_name = f"{folder_name}.zip"

        # Create response with ZIP file
        return send_file(
            zip_buffer,
            mimetype='application/zip',
            as_attachment=True,
            download_name=zip_name
        )

    except Exception as e:
        return jsonify({'error': str(e)}), 500


if __name__ == '__main__':
    # Run the Flask app
    port = int(os.environ.get('PORT', 5002))
    debug = os.environ.get('DEBUG', 'True').lower() == 'true'

    print(f"Starting Terraform Builder API on port {port}")
    print(f"Debug mode: {debug}")

    app.run(host='0.0.0.0', port=port, debug=debug)

# Made with Bob
Enter fullscreen mode Exit fullscreen mode

Diagram Interpretation: Parsing the Draw.io XML

Draw.io architecture diagrams are essentially saved as structured XML files. The backend utilizes the standard xml.etree.ElementTree library to read this XML and extract the critical components. It parses the nested mxCell elements to identify individual cloud resources based on specific, predefined shape patterns and styles (e.g., IBM Cloud shapes). This interpreter doesn't just list components; it also detects connections between them to understand the relationships and generate resource summaries, laying the foundation for accurate script generation.

# drawio_parser.py - Parser for Draw.io XML files containing IBM Cloud diagrams
"""
Draw.io XML Parser for IBM Cloud Diagrams
Parses Draw.io XML files and extracts IBM Cloud components
"""
import xml.etree.ElementTree as ET
from typing import Dict, List, Any


class DrawioParser:
    """Parser for Draw.io XML files containing cloud diagrams"""

    # Mapping of Draw.io shapes to generic resource types
    # These will be converted to provider-specific resources by the generator
    CLOUD_SHAPES = {
        # Core Cloud resources
        'mxgraph.ibm_cloud.ibm-cloud': 'resource_group',
        'mxgraph.ibm.box;prType=cloud': 'resource_group',

        # VPC and Networking
        'mxgraph.ibm_cloud.ibm-cloud--vpc': 'vpc',
        'mxgraph.ibm.box;prType=vpc': 'vpc',
        'mxgraph.ibm.vpc': 'vpc',
        'mxgraph.ibm_cloud.ibm-cloud--subnets': 'subnet',
        'mxgraph.ibm_cloud.floating-ip': 'floating_ip',
        'mxgraph.ibm_cloud.load-balancer--vpc': 'load_balancer',
        'mxgraph.ibm_cloud.network--public': 'public_gateway',

        # Compute
        'mxgraph.ibm_cloud.server--proxy': 'compute_instance',
        'mxgraph.ibm_cloud.application--web': 'app_service',

        # Storage and Databases
        'mxgraph.ibm_cloud.data--base': 'database',

        # Watson Services (IBM-specific, will be skipped for other providers)
        'mxgraph.ibm_cloud.ibm-watsonx--orchestrate': 'watson_service',
        'mxgraph.ibm_cloud.watsonx-governance': 'watson_service',
        'mxgraph.ibm_cloud.watsonx-ai': 'watson_service',

        # Zones (for organizational purposes)
        'mxgraph.ibm.box;prType=zone': 'zone',
    }

    # Service names for IBM Cloud resources
    SERVICE_NAMES = {
        'watsonx--orchestrate': 'watsonx-orchestrate',
        'watsonx-governance': 'watsonx-governance',
        'watsonx-ai': 'watsonx-ai',
    }

    def __init__(self):
        self.components = []
        self.connections = []

    def parse_file(self, file_path: str) -> Dict[str, Any]:
        """Parse a Draw.io XML file and extract IBM Cloud components"""
        try:
            tree = ET.parse(file_path)
            root = tree.getroot()

            # Find all mxCell elements
            for diagram in root.findall('.//diagram'):
                model = diagram.find('.//mxGraphModel')
                if model is not None:
                    self._parse_model(model)

            return {
                'components': self.components,
                'connections': self.connections
            }
        except Exception as e:
            raise Exception(f"Error parsing Draw.io file: {str(e)}")

    def parse_content(self, xml_content: str) -> Dict[str, Any]:
        """Parse Draw.io XML content string"""
        try:
            # Log the first 200 characters for debugging
            print(f"[DEBUG] Received XML content (first 200 chars): {xml_content[:200]}")
            print(f"[DEBUG] XML content length: {len(xml_content)}")
            print(f"[DEBUG] XML content type: {type(xml_content)}")

            # Validate content
            if not xml_content or not xml_content.strip():
                raise Exception("Empty XML content received")

            if not xml_content.strip().startswith('<'):
                raise Exception(f"Not a diagram file (error on line 1 at column 1: Start tag expected, '<' not found). Received: {xml_content[:100]}")

            root = ET.fromstring(xml_content)

            # Find all mxCell elements
            for diagram in root.findall('.//diagram'):
                model = diagram.find('.//mxGraphModel')
                if model is not None:
                    self._parse_model(model)

            return {
                'components': self.components,
                'connections': self.connections
            }
        except ET.ParseError as e:
            print(f"[ERROR] XML Parse Error: {str(e)}")
            print(f"[ERROR] Content that failed: {xml_content[:500]}")
            raise Exception(f"Not a diagram file (error on line 1 at column 1: Start tag expected, '<' not found)")
        except Exception as e:
            print(f"[ERROR] General parsing error: {str(e)}")
            raise Exception(f"Error parsing Draw.io content: {str(e)}")

    def _parse_model(self, model: ET.Element):
        """Parse the mxGraphModel element"""
        root_element = model.find('.//root')
        if root_element is None:
            return

        for cell in root_element.findall('.//mxCell'):
            self._parse_cell(cell)

    def _parse_cell(self, cell: ET.Element):
        """Parse individual mxCell elements"""
        cell_id = cell.get('id')
        if not cell_id:
            return

        value = cell.get('value', '')
        style = cell.get('style', '')

        # Check if this is a cloud component
        for shape_key, resource_type in self.CLOUD_SHAPES.items():
            if shape_key in style:
                component = self._extract_component(cell_id, value, style, shape_key, resource_type)
                if component:
                    self.components.append(component)
                break

        # Check for connections (edges)
        source = cell.get('source')
        target = cell.get('target')
        if source and target:
            self.connections.append({
                'source': source,
                'target': target,
                'id': cell_id
            })

    def _extract_component(self, cell_id: str, value: str, style: str, shape_key: str, resource_type: str) -> Dict[str, Any]:
        """Extract component information from a cell"""
        # Determine the service name
        service_name = None
        for key, service in self.SERVICE_NAMES.items():
            if key in value.lower() or key in shape_key:
                service_name = service
                break

        # Extract geometry if available
        geometry = self._extract_geometry(style)

        component = {
            'id': cell_id,
            'name': value.strip() if value else self._generate_name(shape_key),
            'type': resource_type,
            'shape': shape_key,
            'service': service_name,
            'geometry': geometry
        }

        return component

    def _extract_geometry(self, style: str) -> Dict[str, Any]:
        """Extract geometry information from style string"""
        geometry = {}
        # This is a simplified extraction - can be enhanced
        return geometry

    def _generate_name(self, shape_key: str) -> str:
        """Generate a default name based on the shape type"""
        # Extract the last part of the shape key
        parts = shape_key.split('.')
        if len(parts) > 0:
            name = parts[-1].replace('-', '_').replace('__', '_')
            return name
        return 'unnamed_resource'

    def get_resource_summary(self) -> Dict[str, int]:
        """Get a summary of resources by type"""
        summary = {}
        for component in self.components:
            resource_type = component['type']
            summary[resource_type] = summary.get(resource_type, 0) + 1
        return summary

# Made with Bob
Enter fullscreen mode Exit fullscreen mode

Code Generation: Turning Components into Terraform


Once the diagrams are decoded into component and connection lists, the backend initiates the script generation phase. It programmatically creates three core Terraform configuration files: main.tf for main resources, variables.tf for parameterization, and providers.tf for provider definitions. This process involves sophisticated logic to map Draw.io shapes to corresponding Terraform resource blocks, sanitize names, manage variables, and format the output according to the HashiCorp Configuration Language (HCL). The system is built for modularity, allowing it to generate standardized code for various resource types and cloud providers.

# terraform_generator.py - Generates Terraform code from parsed diagrams
"""
Terraform Code Generator for IBM Cloud Resources
Generates Terraform configuration files from parsed Draw.io diagrams
"""
from typing import Dict, List, Any
import json


class TerraformGenerator:
    """Generates Terraform configuration for multiple cloud providers"""

    # Resource type mappings: generic type -> provider-specific resource type
    RESOURCE_MAPPINGS = {
        'vpc': {
            'ibm': 'ibm_is_vpc',
            'aws': 'aws_vpc',
            'google': 'google_compute_network',
            'azure': 'azurerm_virtual_network',
            'kubernetes': None  # K8s doesn't have VPC concept
        },
        'subnet': {
            'ibm': 'ibm_is_subnet',
            'aws': 'aws_subnet',
            'google': 'google_compute_subnetwork',
            'azure': 'azurerm_subnet',
            'kubernetes': None
        },
        'compute_instance': {
            'ibm': 'ibm_is_instance',
            'aws': 'aws_instance',
            'google': 'google_compute_instance',
            'azure': 'azurerm_virtual_machine',
            'kubernetes': 'kubernetes_deployment'
        },
        'load_balancer': {
            'ibm': 'ibm_is_lb',
            'aws': 'aws_lb',
            'google': 'google_compute_forwarding_rule',
            'azure': 'azurerm_lb',
            'kubernetes': 'kubernetes_service'
        },
        'database': {
            'ibm': 'ibm_database',
            'aws': 'aws_db_instance',
            'google': 'google_sql_database_instance',
            'azure': 'azurerm_postgresql_server',
            'kubernetes': 'kubernetes_stateful_set'
        },
        'resource_group': {
            'ibm': 'ibm_resource_group',
            'aws': None,  # AWS uses tags instead
            'google': 'google_project',
            'azure': 'azurerm_resource_group',
            'kubernetes': 'kubernetes_namespace'
        },
        'floating_ip': {
            'ibm': 'ibm_is_floating_ip',
            'aws': 'aws_eip',
            'google': 'google_compute_address',
            'azure': 'azurerm_public_ip',
            'kubernetes': None
        },
        'public_gateway': {
            'ibm': 'ibm_is_public_gateway',
            'aws': 'aws_nat_gateway',
            'google': 'google_compute_router_nat',
            'azure': 'azurerm_nat_gateway',
            'kubernetes': None
        },
        'app_service': {
            'ibm': 'ibm_resource_instance',
            'aws': 'aws_elastic_beanstalk_application',
            'google': 'google_app_engine_application',
            'azure': 'azurerm_app_service',
            'kubernetes': 'kubernetes_deployment'
        },
        'watson_service': {
            'ibm': 'ibm_resource_instance',
            'aws': None,  # AWS doesn't have Watson equivalent
            'google': None,  # Google doesn't have Watson equivalent
            'azure': None,  # Azure doesn't have Watson equivalent
            'kubernetes': None
        },
        'zone': {
            'ibm': None,  # Organizational only
            'aws': None,
            'google': 'google_compute_zone',
            'azure': None,
            'kubernetes': None
        }
    }

    # Provider configurations
    PROVIDER_CONFIGS = {
        'ibm': {
            'source': 'IBM-Cloud/ibm',
            'version': '>= 1.60.0',
            'docs': 'https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs'
        },
        'azure': {
            'source': 'hashicorp/azurerm',
            'version': '>= 3.0.0',
            'docs': 'https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs'
        },
        'google': {
            'source': 'hashicorp/google',
            'version': '>= 5.0.0',
            'docs': 'https://registry.terraform.io/providers/hashicorp/google/latest/docs'
        },
        'aws': {
            'source': 'hashicorp/aws',
            'version': '>= 5.0.0',
            'docs': 'https://registry.terraform.io/providers/hashicorp/aws/latest/docs'
        },
        'kubernetes': {
            'source': 'hashicorp/kubernetes',
            'version': '>= 2.0.0',
            'docs': 'https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs'
        },
        'terraform': {
            'source': 'hashicorp/tfe',
            'version': '>= 0.50.0',
            'docs': 'https://registry.terraform.io/providers/hashicorp/tfe/latest/docs'
        },
        'instana': {
            'source': 'instana/instana',
            'version': '>= 3.0.0',
            'docs': 'https://registry.terraform.io/providers/instana/instana/latest/docs'
        },
        'vault': {
            'source': 'hashicorp/vault',
            'version': '>= 4.0.0',
            'docs': 'https://registry.terraform.io/providers/hashicorp/vault/latest/docs'
        },
        'turbonomic': {
            'source': 'IBM/turbonomic',
            'version': '>= 1.0.0',
            'docs': 'https://registry.terraform.io/providers/IBM/turbonomic/latest/docs'
        }
    }

    def __init__(self, provider='ibm', custom_provider_url=None):
        self.provider = provider
        self.custom_provider_url = custom_provider_url
        self.custom_provider_config = None

        # If custom provider URL is provided, parse it
        if custom_provider_url and provider == 'other':
            self.custom_provider_config = self._parse_custom_provider_url(custom_provider_url)
            if self.custom_provider_config:
                self.provider = self.custom_provider_config['name']

        self.resources = []
        self.variables = {}
        self.providers = {}

    def _parse_custom_provider_url(self, url: str) -> Dict[str, str] | None:
        """Parse a GitHub provider URL and extract provider information

        Expected format: https://github.com/{org}/{repo}
        Where repo is typically: terraform-provider-{name}

        Returns:
            Dict with 'name', 'source', 'version', and 'docs' keys
        """
        import re

        # Match GitHub URL pattern
        pattern = r'https://github\.com/([\w-]+)/(terraform-provider-([\w-]+))'
        match = re.match(pattern, url)

        if not match:
            return None

        org = match.group(1)
        repo = match.group(2)
        provider_name = match.group(3)

        # Build provider configuration
        return {
            'name': provider_name,
            'source': f'{org}/{provider_name}',
            'version': '>= 1.0.0',  # Default version constraint
            'docs': f'https://registry.terraform.io/providers/{org}/{provider_name}/latest/docs',
            'github_url': url
        }

    def generate_from_components(self, components: List[Dict[str, Any]]) -> Dict[str, str]:
        """Generate Terraform files from parsed components"""
        self.resources = []
        self.variables = {}

        # Process each component
        for idx, component in enumerate(components):
            resource = self._generate_resource(component, idx)
            if resource:
                self.resources.append(resource)

        # Generate the three required files
        main_tf = self._generate_main_tf()
        variables_tf = self._generate_variables_tf()
        providers_tf = self._generate_providers_tf()

        return {
            'main.tf': main_tf,
            'variables.tf': variables_tf,
            'providers.tf': providers_tf
        }

    def _generate_resource(self, component: Dict[str, Any], idx: int) -> Dict[str, Any] | None:
        """Generate a Terraform resource from a component"""
        generic_type = component.get('type')  # Generic type from parser
        name = component.get('name', f'resource_{idx}')
        service = component.get('service')

        # Sanitize name for Terraform
        tf_name = self._sanitize_name(name)

        # Get provider-specific resource type from mapping
        if generic_type not in self.RESOURCE_MAPPINGS:
            return None

        provider_resource_type = self.RESOURCE_MAPPINGS[generic_type].get(self.provider)

        # If this resource type is not supported by the provider, skip it
        if provider_resource_type is None:
            if not hasattr(self, 'skipped_resources'):
                self.skipped_resources = []
            self.skipped_resources.append({
                'name': name,
                'type': generic_type,
                'reason': f'Not supported by {self.provider} provider'
            })
            return None

        # Generate provider-specific resource based on generic type
        if generic_type == 'vpc':
            return self._generate_vpc_resource_multi(tf_name, name, provider_resource_type)
        elif generic_type == 'subnet':
            return self._generate_subnet_resource(tf_name, name, provider_resource_type)
        elif generic_type == 'compute_instance':
            return self._generate_compute_resource(tf_name, name, provider_resource_type)
        elif generic_type == 'load_balancer':
            return self._generate_load_balancer_resource(tf_name, name, provider_resource_type)
        elif generic_type == 'database':
            return self._generate_database_resource(tf_name, name, provider_resource_type)
        elif generic_type == 'resource_group':
            return self._generate_resource_group_multi(tf_name, name, provider_resource_type)
        elif generic_type == 'floating_ip':
            return self._generate_floating_ip_resource(tf_name, name, provider_resource_type)
        elif generic_type == 'public_gateway':
            return self._generate_gateway_resource(tf_name, name, provider_resource_type)
        elif generic_type == 'app_service':
            return self._generate_app_service_resource(tf_name, name, provider_resource_type)
        elif generic_type == 'watson_service':
            return self._generate_watson_service_resource(tf_name, name, service or '', provider_resource_type)

        return None

    def _generate_vpc_resource(self, tf_name: str, display_name: str) -> Dict[str, Any]:
        """Generate VPC resource configuration"""
        self.variables[f'{tf_name}_name'] = {
            'description': f'Name for the {display_name} VPC',
            'type': 'string',
            'default': display_name
        }

        return {
            'type': 'ibm_is_vpc',
            'name': tf_name,
            'config': {
                'name': f'var.{tf_name}_name',
                'resource_group': 'var.resource_group_id',
                'tags': ['var.tags']
            }
        }

    def _generate_resource_instance(self, tf_name: str, display_name: str, service: str) -> Dict[str, Any]:
        """Generate resource instance configuration for Watson services"""
        if not service:
            service = 'watsonx-ai'

        # Map service names to IBM Cloud service names
        service_map = {
            'watsonx-orchestrate': 'watsonx-orchestrate',
            'watsonx-governance': 'watsonx-governance',
            'watsonx-ai': 'watsonx-ai'
        }

        service_name = service_map.get(service, service)

        self.variables[f'{tf_name}_name'] = {
            'description': f'Name for the {display_name} instance',
            'type': 'string',
            'default': display_name
        }

        self.variables[f'{tf_name}_plan'] = {
            'description': f'Service plan for {display_name}',
            'type': 'string',
            'default': 'lite'
        }

        service_map = {
            'watsonx-orchestrate': 'watsonx-orchestrate',
            'watsonx-governance': 'watsonx-governance',
            'watsonx-ai': 'watsonx-ai'
        }

        service_name = service_map.get(service, service)

        return {
            'type': 'ibm_resource_instance',
            'name': tf_name,
            'config': {
                'name': f'var.{tf_name}_name',
                'service': f'"{service_name}"',
                'plan': f'var.{tf_name}_plan',
                'location': 'var.region',
                'resource_group_id': 'var.resource_group_id',
                'tags': ['var.tags']
            }
        }

    # Multi-provider resource generators

    def _generate_vpc_resource_multi(self, tf_name: str, display_name: str, resource_type: str) -> Dict[str, Any]:
        """Generate VPC/Network resource for any provider"""
        self.variables[f'{tf_name}_name'] = {
            'description': f'Name for the {display_name} network',
            'type': 'string',
            'default': display_name
        }

        config = {'name': f'var.{tf_name}_name'}

        # Provider-specific configurations
        if self.provider == 'ibm':
            config['resource_group'] = 'var.resource_group_id'
            config['tags'] = 'var.tags'
        elif self.provider == 'aws':
            config['cidr_block'] = '"10.0.0.0/16"'
            config['tags'] = 'var.tags'
        elif self.provider == 'google':
            config['project'] = 'var.project_id'
            config['auto_create_subnetworks'] = 'false'
        elif self.provider == 'azure':
            config['location'] = 'var.location'
            config['resource_group_name'] = 'azurerm_resource_group.main.name'
            config['address_space'] = '["10.0.0.0/16"]'
            config['tags'] = 'var.tags'

        return {'type': resource_type, 'name': tf_name, 'config': config}

    def _generate_subnet_resource(self, tf_name: str, display_name: str, resource_type: str) -> Dict[str, Any]:
        """Generate subnet resource for any provider"""
        self.variables[f'{tf_name}_name'] = {
            'description': f'Name for the {display_name} subnet',
            'type': 'string',
            'default': display_name
        }

        config = {'name': f'var.{tf_name}_name'}

        if self.provider == 'ibm':
            config['vpc'] = 'ibm_is_vpc.main.id'
            config['zone'] = '"us-south-1"'
            config['ipv4_cidr_block'] = '"10.0.1.0/24"'
        elif self.provider == 'aws':
            config['vpc_id'] = 'aws_vpc.main.id'
            config['cidr_block'] = '"10.0.1.0/24"'
            config['availability_zone'] = '"us-east-1a"'
            config['tags'] = 'var.tags'
        elif self.provider == 'google':
            config['network'] = 'google_compute_network.main.id'
            config['ip_cidr_range'] = '"10.0.1.0/24"'
            config['region'] = 'var.region'
        elif self.provider == 'azure':
            config['resource_group_name'] = 'azurerm_resource_group.main.name'
            config['virtual_network_name'] = 'azurerm_virtual_network.main.name'
            config['address_prefixes'] = '["10.0.1.0/24"]'

        return {'type': resource_type, 'name': tf_name, 'config': config}

    def _generate_compute_resource(self, tf_name: str, display_name: str, resource_type: str) -> Dict[str, Any]:
        """Generate compute instance resource for any provider"""
        self.variables[f'{tf_name}_name'] = {
            'description': f'Name for the {display_name} instance',
            'type': 'string',
            'default': display_name
        }

        config = {'name': f'var.{tf_name}_name'}

        if self.provider == 'ibm':
            config['profile'] = '"cx2-2x4"'
            config['image'] = '"r006-14140f94-fcc4-11e9-96e7-a72723715315"'
            config['zone'] = '"us-south-1"'
            config['vpc'] = 'ibm_is_vpc.main.id'
            config['primary_network_interface'] = '{subnet = ibm_is_subnet.main.id}'
        elif self.provider == 'aws':
            config['ami'] = '"ami-0c55b159cbfafe1f0"'
            config['instance_type'] = '"t2.micro"'
            config['subnet_id'] = 'aws_subnet.main.id'
            config['tags'] = 'var.tags'
        elif self.provider == 'google':
            config['machine_type'] = '"e2-medium"'
            config['zone'] = '"us-central1-a"'
            config['boot_disk'] = '{initialize_params {image = "debian-cloud/debian-11"}}'
            config['network_interface'] = '{network = google_compute_network.main.id, subnetwork = google_compute_subnetwork.main.id}'
        elif self.provider == 'azure':
            config['location'] = 'var.location'
            config['resource_group_name'] = 'azurerm_resource_group.main.name'
            config['network_interface_ids'] = '[azurerm_network_interface.main.id]'
            config['vm_size'] = '"Standard_B2s"'
            config['os_disk'] = '{caching = "ReadWrite", storage_account_type = "Standard_LRS"}'
        elif self.provider == 'kubernetes':
            config['metadata'] = '{name = var.%s_name, namespace = var.namespace}' % tf_name
            config['spec'] = '{replicas = 2, selector {match_labels = {app = var.%s_name}}, template {metadata {labels = {app = var.%s_name}}, spec {container {name = var.%s_name, image = "nginx:latest"}}}}' % (tf_name, tf_name, tf_name)

        return {'type': resource_type, 'name': tf_name, 'config': config}

    def _generate_load_balancer_resource(self, tf_name: str, display_name: str, resource_type: str) -> Dict[str, Any]:
        """Generate load balancer resource for any provider"""
        self.variables[f'{tf_name}_name'] = {
            'description': f'Name for the {display_name} load balancer',
            'type': 'string',
            'default': display_name
        }

        config = {'name': f'var.{tf_name}_name'}

        if self.provider == 'ibm':
            config['subnets'] = '[ibm_is_subnet.main.id]'
        elif self.provider == 'aws':
            config['load_balancer_type'] = '"application"'
            config['subnets'] = '[aws_subnet.main.id]'
            config['tags'] = 'var.tags'
        elif self.provider == 'google':
            config['ip_protocol'] = '"TCP"'
            config['port_range'] = '"80"'
            config['target'] = 'google_compute_target_pool.main.self_link'
        elif self.provider == 'azure':
            config['location'] = 'var.location'
            config['resource_group_name'] = 'azurerm_resource_group.main.name'
        elif self.provider == 'kubernetes':
            config['metadata'] = '{name = var.%s_name, namespace = var.namespace}' % tf_name
            config['spec'] = '{type = "LoadBalancer", selector = {app = "myapp"}, port {port = 80, target_port = 8080}}'

        return {'type': resource_type, 'name': tf_name, 'config': config}

    def _generate_database_resource(self, tf_name: str, display_name: str, resource_type: str) -> Dict[str, Any]:
        """Generate database resource for any provider"""
        self.variables[f'{tf_name}_name'] = {
            'description': f'Name for the {display_name} database',
            'type': 'string',
            'default': display_name
        }

        config = {'name': f'var.{tf_name}_name'}

        if self.provider == 'ibm':
            config['service'] = '"databases-for-postgresql"'
            config['plan'] = '"standard"'
            config['location'] = 'var.region'
            config['resource_group_id'] = 'var.resource_group_id'
        elif self.provider == 'aws':
            config['engine'] = '"postgres"'
            config['engine_version'] = '"14.7"'
            config['instance_class'] = '"db.t3.micro"'
            config['allocated_storage'] = '20'
            config['username'] = '"admin"'
            config['password'] = '"changeme"'
            config['skip_final_snapshot'] = 'true'
            config['tags'] = 'var.tags'
        elif self.provider == 'google':
            config['database_version'] = '"POSTGRES_14"'
            config['region'] = 'var.region'
            config['settings'] = '{tier = "db-f1-micro"}'
        elif self.provider == 'azure':
            config['location'] = 'var.location'
            config['resource_group_name'] = 'azurerm_resource_group.main.name'
            config['sku_name'] = '"B_Gen5_1"'
            config['version'] = '"11"'
            config['administrator_login'] = '"psqladmin"'
            config['administrator_login_password'] = '"H@Sh1CoR3!"'
        elif self.provider == 'kubernetes':
            config['metadata'] = '{name = var.%s_name, namespace = var.namespace}' % tf_name
            config['spec'] = '{replicas = 1, selector {match_labels = {app = "database"}}, template {metadata {labels = {app = "database"}}, spec {container {name = "postgres", image = "postgres:14"}}}}'

        return {'type': resource_type, 'name': tf_name, 'config': config}

    def _generate_resource_group_multi(self, tf_name: str, display_name: str, resource_type: str) -> Dict[str, Any]:
        """Generate resource group/namespace for any provider"""
        self.variables[f'{tf_name}_name'] = {
            'description': f'Name for the {display_name} resource group',
            'type': 'string',
            'default': display_name
        }

        config = {'name': f'var.{tf_name}_name'}

        if self.provider == 'ibm':
            pass  # IBM resource group only needs name
        elif self.provider == 'google':
            config['project_id'] = f'var.{tf_name}_name'
        elif self.provider == 'azure':
            config['location'] = 'var.location'
        elif self.provider == 'kubernetes':
            config['metadata'] = '{name = var.%s_name}' % tf_name

        return {'type': resource_type, 'name': tf_name, 'config': config}

    def _generate_floating_ip_resource(self, tf_name: str, display_name: str, resource_type: str) -> Dict[str, Any]:
        """Generate floating/elastic IP resource for any provider"""
        self.variables[f'{tf_name}_name'] = {
            'description': f'Name for the {display_name} IP',
            'type': 'string',
            'default': display_name
        }

        config = {'name': f'var.{tf_name}_name'}

        if self.provider == 'ibm':
            config['zone'] = '"us-south-1"'
        elif self.provider == 'aws':
            config['vpc'] = 'true'
            config['tags'] = 'var.tags'
        elif self.provider == 'google':
            config['region'] = 'var.region'
        elif self.provider == 'azure':
            config['location'] = 'var.location'
            config['resource_group_name'] = 'azurerm_resource_group.main.name'
            config['allocation_method'] = '"Static"'

        return {'type': resource_type, 'name': tf_name, 'config': config}

    def _generate_gateway_resource(self, tf_name: str, display_name: str, resource_type: str) -> Dict[str, Any]:
        """Generate NAT gateway resource for any provider"""
        self.variables[f'{tf_name}_name'] = {
            'description': f'Name for the {display_name} gateway',
            'type': 'string',
            'default': display_name
        }

        config = {'name': f'var.{tf_name}_name'}

        if self.provider == 'ibm':
            config['vpc'] = 'ibm_is_vpc.main.id'
            config['zone'] = '"us-south-1"'
        elif self.provider == 'aws':
            config['subnet_id'] = 'aws_subnet.main.id'
            config['allocation_id'] = 'aws_eip.main.id'
            config['tags'] = 'var.tags'
        elif self.provider == 'google':
            config['router'] = 'google_compute_router.main.name'
            config['region'] = 'var.region'
            config['nat_ip_allocate_option'] = '"AUTO_ONLY"'
            config['source_subnetwork_ip_ranges_to_nat'] = '"ALL_SUBNETWORKS_ALL_IP_RANGES"'
        elif self.provider == 'azure':
            config['location'] = 'var.location'
            config['resource_group_name'] = 'azurerm_resource_group.main.name'

        return {'type': resource_type, 'name': tf_name, 'config': config}

    def _generate_app_service_resource(self, tf_name: str, display_name: str, resource_type: str) -> Dict[str, Any]:
        """Generate application service resource for any provider"""
        self.variables[f'{tf_name}_name'] = {
            'description': f'Name for the {display_name} application',
            'type': 'string',
            'default': display_name
        }

        config = {'name': f'var.{tf_name}_name'}

        if self.provider == 'ibm':
            config['service'] = '"cloud-object-storage"'
            config['plan'] = '"lite"'
            config['location'] = 'var.region'
        elif self.provider == 'aws':
            config['description'] = f'"{display_name} application"'
        elif self.provider == 'google':
            config['location_id'] = 'var.region'
        elif self.provider == 'azure':
            config['location'] = 'var.location'
            config['resource_group_name'] = 'azurerm_resource_group.main.name'
            config['app_service_plan_id'] = 'azurerm_app_service_plan.main.id'
        elif self.provider == 'kubernetes':
            config['metadata'] = '{name = var.%s_name, namespace = var.namespace}' % tf_name
            config['spec'] = '{replicas = 3, selector {match_labels = {app = var.%s_name}}, template {metadata {labels = {app = var.%s_name}}, spec {container {name = var.%s_name, image = "nginx:latest", port {container_port = 80}}}}}' % (tf_name, tf_name, tf_name)

        return {'type': resource_type, 'name': tf_name, 'config': config}

    def _generate_watson_service_resource(self, tf_name: str, display_name: str, service: str, resource_type: str) -> Dict[str, Any] | None:
        """Generate Watson service resource (IBM only)"""
        if self.provider != 'ibm':
            return None

        return self._generate_resource_instance(tf_name, display_name, service or 'watsonx-ai')

    def _generate_resource_group(self, tf_name: str, display_name: str) -> Dict[str, Any]:
        """Generate resource group configuration"""
        self.variables[f'{tf_name}_name'] = {
            'description': f'Name for the {display_name} resource group',
            'type': 'string',
            'default': display_name
        }

        return {
            'type': 'ibm_resource_group',
            'name': tf_name,
            'config': {
                'name': f'var.{tf_name}_name'
            }
        }

    def _generate_main_tf(self) -> str:
        """Generate main.tf content"""
        # Use custom provider config if available, otherwise use predefined
        if self.custom_provider_config:
            provider_config = self.custom_provider_config
            provider_name = self.custom_provider_config['name']
        else:
            provider_config = self.PROVIDER_CONFIGS.get(self.provider, self.PROVIDER_CONFIGS['ibm'])
            provider_name = self.provider

        lines = [
            f'# {provider_name.upper()} Terraform Configuration',
            '# Generated from Draw.io diagram',
            f'# Provider: {provider_name}',
            f'# Documentation: {provider_config["docs"]}',
            '',
            '# Terraform configuration',
            'terraform {',
            '  required_version = ">= 1.0"',
            '  required_providers {',
            f'    {provider_name} = {{',
            f'      source  = "{provider_config["source"]}"',
            f'      version = "{provider_config["version"]}"',
            '    }',
            '  }',
            '}',
            ''
        ]

        # Add resources
        if self.resources:
            for resource in self.resources:
                lines.extend(self._format_resource(resource))
                lines.append('')
        else:
            # No resources generated - add helpful message
            lines.append('# No resources generated.')
            if hasattr(self, 'skipped_resources') and self.skipped_resources:
                lines.append('# This diagram contains IBM Cloud-specific components that are not')
                lines.append(f'# supported by the {provider_name} provider.')
                lines.append('#')
                lines.append('# Skipped IBM Cloud resources:')
                for skipped in self.skipped_resources:
                    lines.append(f'#   - {skipped["name"]} ({skipped["type"]})')
                lines.append('#')
                lines.append('# To generate IBM Cloud resources, please select "IBM Cloud" as the provider.')
            lines.append('')

        return '\n'.join(lines)

    def _format_resource(self, resource: Dict[str, Any]) -> List[str]:
        """Format a resource as Terraform HCL"""
        lines = [
            f'resource "{resource["type"]}" "{resource["name"]}" {{'
        ]

        for key, value in resource['config'].items():
            if isinstance(value, list):
                lines.append(f'  {key} = {value[0]}')
            else:
                lines.append(f'  {key} = {value}')

        lines.append('}')
        return lines

    def _generate_variables_tf(self) -> str:
        """Generate variables.tf content"""
        provider_vars = self._get_provider_variables()

        lines = [
            f'# Variables for {self.provider.upper()} resources',
            '# Generated from Draw.io diagram',
            ''
        ]

        # Add provider-specific variables
        lines.extend(provider_vars)
        lines.append('')

        # Add component-specific variables
        for var_name, var_config in self.variables.items():
            lines.append(f'variable "{var_name}" {{')
            lines.append(f'  description = "{var_config["description"]}"')
            lines.append(f'  type        = {var_config["type"]}')
            if 'default' in var_config:
                if isinstance(var_config['default'], str):
                    lines.append(f'  default     = "{var_config["default"]}"')
                else:
                    lines.append(f'  default     = {var_config["default"]}')
            lines.append('}')
            lines.append('')

        return '\n'.join(lines)

    def _generate_providers_tf(self) -> str:
        """Generate providers.tf content"""
        provider_config = self._get_provider_config()

        lines = [
            f'# {self.provider.upper()} Provider Configuration',
            '# Generated from Draw.io diagram',
            ''
        ]

        lines.extend(provider_config)
        lines.append('')

        return '\n'.join(lines)

    def _get_provider_variables(self) -> List[str]:
        """Get provider-specific variables"""
        # For custom providers, return generic variables
        if self.custom_provider_config:
            return [
                'variable "api_key" {',
                '  description = "API Key for authentication"',
                '  type        = string',
                '  sensitive   = true',
                '}',
                '',
                'variable "region" {',
                '  description = "Region for resources"',
                '  type        = string',
                '  default     = "us-south"',
                '}',
                '',
                'variable "tags" {',
                '  description = "Tags for resources"',
                '  type        = list(string)',
                '  default     = ["terraform", "drawio-generated"]',
                '}'
            ]

        if self.provider == 'ibm':
            return [
                'variable "ibmcloud_api_key" {',
                '  description = "IBM Cloud API Key"',
                '  type        = string',
                '  sensitive   = true',
                '}',
                '',
                'variable "region" {',
                '  description = "IBM Cloud region"',
                '  type        = string',
                '  default     = "us-south"',
                '}',
                '',
                'variable "resource_group_id" {',
                '  description = "Resource group ID"',
                '  type        = string',
                '}',
                '',
                'variable "tags" {',
                '  description = "Tags for resources"',
                '  type        = list(string)',
                '  default     = ["terraform", "drawio-generated"]',
                '}'
            ]
        elif self.provider == 'azure':
            return [
                'variable "subscription_id" {',
                '  description = "Azure Subscription ID"',
                '  type        = string',
                '}',
                '',
                'variable "tenant_id" {',
                '  description = "Azure Tenant ID"',
                '  type        = string',
                '}',
                '',
                'variable "location" {',
                '  description = "Azure region"',
                '  type        = string',
                '  default     = "eastus"',
                '}',
                '',
                'variable "tags" {',
                '  description = "Tags for resources"',
                '  type        = map(string)',
                '  default     = {',
                '    Environment = "Development"',
                '    ManagedBy   = "Terraform"',
                '  }',
                '}'
            ]
        elif self.provider == 'google':
            return [
                'variable "project_id" {',
                '  description = "Google Cloud Project ID"',
                '  type        = string',
                '}',
                '',
                'variable "region" {',
                '  description = "Google Cloud region"',
                '  type        = string',
                '  default     = "us-central1"',
                '}',
                '',
                'variable "labels" {',
                '  description = "Labels for resources"',
                '  type        = map(string)',
                '  default     = {',
                '    environment = "development"',
                '    managed_by  = "terraform"',
                '  }',
                '}'
            ]
        elif self.provider == 'aws':
            return [
                'variable "region" {',
                '  description = "AWS region"',
                '  type        = string',
                '  default     = "us-east-1"',
                '}',
                '',
                'variable "tags" {',
                '  description = "Tags for resources"',
                '  type        = map(string)',
                '  default     = {',
                '    Environment = "Development"',
                '    ManagedBy   = "Terraform"',
                '  }',
                '}'
            ]
        elif self.provider == 'kubernetes':
            return [
                'variable "config_path" {',
                '  description = "Path to kubeconfig file"',
                '  type        = string',
                '  default     = "~/.kube/config"',
                '}',
                '',
                'variable "config_context" {',
                '  description = "Kubernetes context to use"',
                '  type        = string',
                '  default     = ""',
                '}',
                '',
                'variable "namespace" {',
                '  description = "Default namespace"',
                '  type        = string',
                '  default     = "default"',
                '}'
            ]
        elif self.provider == 'terraform':
            return [
                'variable "tfe_token" {',
                '  description = "Terraform Cloud/Enterprise API Token"',
                '  type        = string',
                '  sensitive   = true',
                '}',
                '',
                'variable "tfe_hostname" {',
                '  description = "Terraform Cloud/Enterprise hostname"',
                '  type        = string',
                '  default     = "app.terraform.io"',
                '}',
                '',
                'variable "organization" {',
                '  description = "Terraform Cloud/Enterprise organization name"',
                '  type        = string',
                '}'
            ]

        return []

    def _get_provider_config(self) -> List[str]:
        """Get provider-specific configuration"""
        # For custom providers, return generic configuration
        if self.custom_provider_config:
            provider_name = self.custom_provider_config['name']
            return [
                f'provider "{provider_name}" {{',
                '  # Configure provider-specific settings here',
                '  # Refer to provider documentation for available options',
                f'  # Documentation: {self.custom_provider_config["docs"]}',
                '}'
            ]

        if self.provider == 'ibm':
            return [
                'provider "ibm" {',
                '  ibmcloud_api_key = var.ibmcloud_api_key',
                '  region           = var.region',
                '}'
            ]
        elif self.provider == 'azure':
            return [
                'provider "azurerm" {',
                '  features {}',
                '  subscription_id = var.subscription_id',
                '  tenant_id       = var.tenant_id',
                '}'
            ]
        elif self.provider == 'google':
            return [
                'provider "google" {',
                '  project = var.project_id',
                '  region  = var.region',
                '}'
            ]
        elif self.provider == 'aws':
            return [
                'provider "aws" {',
                '  region = var.region',
                '}'
            ]
        elif self.provider == 'kubernetes':
            return [
                'provider "kubernetes" {',
                '  config_path    = var.config_path',
                '  config_context = var.config_context',
                '}'
            ]
        elif self.provider == 'terraform':
            return [
                'provider "tfe" {',
                '  token    = var.tfe_token',
                '  hostname = var.tfe_hostname',
                '}'
            ]

        return []

    def _sanitize_name(self, name: str) -> str:
        """Sanitize a name for use in Terraform"""
        # Remove special characters and replace spaces with underscores
        sanitized = name.lower()
        sanitized = sanitized.replace(' ', '_')
        sanitized = sanitized.replace('-', '_')
        sanitized = sanitized.replace('.', '_')
        # Remove any non-alphanumeric characters except underscores
        sanitized = ''.join(c for c in sanitized if c.isalnum() or c == '_')
        # Ensure it starts with a letter
        if sanitized and not sanitized[0].isalpha():
            sanitized = 'res_' + sanitized
        return sanitized or 'unnamed_resource'

# Made with Bob
Enter fullscreen mode Exit fullscreen mode

Strategic Versatility: From a Curated List to Custom Templates


Current versions of the application excel at interpreting IBM Cloud-specific shapes and generating highly accurate HCL for that environment (to be tested for sure). However, this is only the beginning of our multi-cloud vision. Future iterations will expand this specialized support to other major providers like AWS and Azure, moving beyond generic detection to deep resource mapping. We are also developing a robust “Generator Template” feature that will allow users to extend the system’s capabilities independently; by simply providing a repository URL for any GitHub-hosted Terraform provider, users will eventually be able to generate bespoke configurations for even the most niche infrastructure requirements.


The Front-end — HTML And Javascript

The frontend of the Terraform Builder is a modern, responsive single-page application built using a standard web stack of HTML5, CSS3, and vanilla JavaScript. The interface, defined in index.html, features a clean two-column layout that guides users through the workflow of either creating diagrams in Draw.io or uploading existing XML files. Styling is managed through styles.css, which implements a professional look-and-feel inspired by the IBM Design System, utilizing the IBM Plex font family. The client-side logic resides in app.js, which orchestrates the user interaction—handling drag-and-drop file uploads, managing the provider selection (including custom GitHub templates), and dynamically updating the UI with generated HCL code. This JavaScript layer also facilitates the multi-file preview and download functionality, ensuring a seamless experience without the need for complex frontend frameworks.


Output Samples


The application streamlines the transition from design to deployment by delivering the generated HCL scripts in a structured, timestamped format, ensuring every architectural iteration is captured and organized. By programmatically mapping visual components from a Draw.io diagram directly into main.tf, variables.tf, and providers.tf, the tool dramatically accelerates the infrastructure provisioning process, eliminating the manual overhead of writing boilerplate code. However, while this visual-to-code automation provides a high-speed starting point that reflects the intended design, it is crucial that these scripts are thoroughly reviewed and tested in a development environment before production use. This workflow allows architects to focus on the high-level logic while the application handles the technical translation, significantly reducing time-to-market without bypassing the essential step of human validation.

# IBM Terraform Configuration
# Generated from Draw.io diagram
# Provider: ibm
# Documentation: https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs

# Terraform configuration
terraform {
  required_version = ">= 1.0"
  required_providers {
    ibm = {
      source  = "IBM-Cloud/ibm"
      version = ">= 1.60.0"
    }
  }
}

resource "ibm_resource_group" "ibm_cloud" {
  name = var.ibm_cloud_name
}

resource "ibm_is_vpc" "vpc" {
  name = var.vpc_name
  resource_group = var.resource_group_id
  tags = var.tags
}

resource "ibm_resource_group" "ibm_cloud" {
  name = var.ibm_cloud_name
}

resource "ibm_database" "data_base" {
  name = var.data_base_name
  service = "databases-for-postgresql"
  plan = "standard"
  location = var.region
  resource_group_id = var.resource_group_id
}

resource "ibm_is_public_gateway" "network_public" {
  name = var.network_public_name
  vpc = ibm_is_vpc.main.id
  zone = "us-south-1"
}

resource "ibm_is_instance" "rev_" {
  name = var.rev__name
  profile = "cx2-2x4"
  image = "r006-14140f94-fcc4-11e9-96e7-a72723715315"
  zone = "us-south-1"
  vpc = ibm_is_vpc.main.id
  primary_network_interface = {subnet = ibm_is_subnet.main.id}
}

resource "ibm_resource_instance" "application_web" {
  name = var.application_web_name
  service = "cloud-object-storage"
  plan = "lite"
  location = var.region
}

resource "ibm_resource_group" "ibm_cloud" {
  name = var.ibm_cloud_name
}

resource "ibm_database" "data_base" {
  name = var.data_base_name
  service = "databases-for-postgresql"
  plan = "standard"
  location = var.region
  resource_group_id = var.resource_group_id
}

resource "ibm_is_public_gateway" "network_public" {
  name = var.network_public_name
  vpc = ibm_is_vpc.main.id
  zone = "us-south-1"
}

resource "ibm_is_instance" "rev_" {
  name = var.rev__name
  profile = "cx2-2x4"
  image = "r006-14140f94-fcc4-11e9-96e7-a72723715315"
  zone = "us-south-1"
  vpc = ibm_is_vpc.main.id
  primary_network_interface = {subnet = ibm_is_subnet.main.id}
}

resource "ibm_is_floating_ip" "floating_ip" {
  name = var.floating_ip_name
  zone = "us-south-1"
}

resource "ibm_resource_instance" "application_web" {
  name = var.application_web_name
  service = "cloud-object-storage"
  plan = "lite"
  location = var.region
}

resource "ibm_is_lb" "load_balancer_vpc" {
  name = var.load_balancer_vpc_name
  subnets = [ibm_is_subnet.main.id]
}

resource "ibm_is_floating_ip" "floating_ip" {
  name = var.floating_ip_name
  zone = "us-south-1"
}
Enter fullscreen mode Exit fullscreen mode
# IBM Provider Configuration
# Generated from Draw.io diagram

provider "ibm" {
  ibmcloud_api_key = var.ibmcloud_api_key
  region           = var.region
}
Enter fullscreen mode Exit fullscreen mode
# Variables for IBM resources
# Generated from Draw.io diagram

variable "ibmcloud_api_key" {
  description = "IBM Cloud API Key"
  type        = string
  sensitive   = true
}

variable "region" {
  description = "IBM Cloud region"
  type        = string
  default     = "us-south"
}

variable "resource_group_id" {
  description = "Resource group ID"
  type        = string
}

variable "tags" {
  description = "Tags for resources"
  type        = list(string)
  default     = ["terraform", "drawio-generated"]
}

variable "ibm_cloud_name" {
  description = "Name for the ibm_cloud resource group"
  type        = string
  default     = "ibm_cloud"
}

variable "vpc_name" {
  description = "Name for the VPC network"
  type        = string
  default     = "VPC"
}

variable "data_base_name" {
  description = "Name for the data_base database"
  type        = string
  default     = "data_base"
}

variable "network_public_name" {
  description = "Name for the network_public gateway"
  type        = string
  default     = "network_public"
}

variable "rev__name" {
  description = "Name for the Rev. instance"
  type        = string
  default     = "Rev."
}

variable "application_web_name" {
  description = "Name for the application_web application"
  type        = string
  default     = "application_web"
}

variable "floating_ip_name" {
  description = "Name for the floating_ip IP"
  type        = string
  default     = "floating_ip"
}

variable "load_balancer_vpc_name" {
  description = "Name for the load_balancer_vpc load balancer"
  type        = string
  default     = "load_balancer_vpc"
}
Enter fullscreen mode Exit fullscreen mode

Deployment

Beyond local execution, the Terraform Builder is engineered for seamless cloud-native deployment. The inclusion of a multi-stage Dockerfile ensures the application is fully containerized, providing a consistent environment across different hosting platforms. For teams utilizing orchestration, the project provides a complete set of Kubernetes manifests: deployment.yaml defines a scalable architecture with dual replicas and integrated liveness/readiness probes for high availability, while configmap.yaml centralizes environment-specific settings. This "production-ready" approach allows the generator to be easily hosted on platforms like IBM Cloud Code Engine, Red Hat OpenShift, or any standard Kubernetes cluster, transforming it from a local utility into a shared enterprise service.


Conclusion

In conclusion, the Terraform Builder provides a comprehensive, end-to-end bridge between visual architectural design and automated infrastructure deployment. By combining a sleek, user friendly frontend with a powerful Python-driven backend, the application successfully transforms static Draw.io diagrams into functional, multi-file Terraform configurations. With its production-ready containerization and Kubernetes manifests, the tool is prepared for immediate deployment in cloud-native environments. While currently optimized for IBM Cloud in its alpha release, the foundational architecture is already in place to support a vast ecosystem of providers and custom templates, representing a significant step forward in personal productivity and the democratization of Infrastructure as Code.

>>> Thanks for reading <<<

Links

Top comments (0)