# Feature 6: HAProxy Import (Custom Parser)

**Status:** 🔄 Geplant
**Aufwand:** 2 Wochen
**Release:** v1.3.0-beta2 (Woche 8)
**Priorität:** 🔴 Hoch

## Übersicht

Import von HAProxy haproxy.cfg nach GAL. HAProxy verwendet ein eigenes Konfigurations-Format (ähnlich nginx.conf), daher ist ein **Custom Parser** erforderlich. HAProxy ist der de-facto Standard für Load Balancing und daher kritisch.

## Herausforderungen

- **Eigenes Format**: haproxy.cfg hat section-based Syntax (global, defaults, frontend, backend)
- **ACL-Syntax**: HAProxy ACLs sind komplex und vielfältig
- **Stick-Tables**: Rate limiting via stick-tables (komplex)
- **http-request/http-response**: Vielfältige Direktiven
- **Multi-line Syntax**: Optionen können über mehrere Zeilen gehen

## Implementierung

### Custom Parser: HAProxyConfigParser

```python
# gal/parsers/haproxy_parser.py

import re
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
from enum import Enum

class SectionType(Enum):
    GLOBAL = "global"
    DEFAULTS = "defaults"
    FRONTEND = "frontend"
    BACKEND = "backend"
    LISTEN = "listen"

@dataclass
class HAProxySection:
    """Represents a HAProxy configuration section."""
    type: SectionType
    name: Optional[str]  # None for global/defaults
    directives: List[Dict[str, Any]]

class HAProxyConfigParser:
    """Parser for haproxy.cfg files.

    Parses HAProxy configuration files into structured sections
    for conversion to GAL.
    """

    def __init__(self):
        self.lines = []
        self.pos = 0

    def parse(self, config_text: str) -> List[HAProxySection]:
        """Parse haproxy.cfg into sections.

        Args:
            config_text: haproxy.cfg content

        Returns:
            List of HAProxySection objects

        Raises:
            ValueError: If syntax is invalid
        """
        # Preprocess
        lines = self._preprocess(config_text)
        self.lines = lines
        self.pos = 0

        sections = []
        current_section = None

        while self.pos < len(self.lines):
            line = self.lines[self.pos].strip()

            # Check for section start
            if line.startswith("global"):
                if current_section:
                    sections.append(current_section)
                current_section = HAProxySection(
                    type=SectionType.GLOBAL,
                    name=None,
                    directives=[]
                )
                self.pos += 1

            elif line.startswith("defaults"):
                if current_section:
                    sections.append(current_section)
                current_section = HAProxySection(
                    type=SectionType.DEFAULTS,
                    name=None,
                    directives=[]
                )
                self.pos += 1

            elif line.startswith("frontend"):
                if current_section:
                    sections.append(current_section)
                parts = line.split(maxsplit=1)
                name = parts[1] if len(parts) > 1 else "unnamed"
                current_section = HAProxySection(
                    type=SectionType.FRONTEND,
                    name=name,
                    directives=[]
                )
                self.pos += 1

            elif line.startswith("backend"):
                if current_section:
                    sections.append(current_section)
                parts = line.split(maxsplit=1)
                name = parts[1] if len(parts) > 1 else "unnamed"
                current_section = HAProxySection(
                    type=SectionType.BACKEND,
                    name=name,
                    directives=[]
                )
                self.pos += 1

            elif line.startswith("listen"):
                if current_section:
                    sections.append(current_section)
                parts = line.split(maxsplit=1)
                name = parts[1] if len(parts) > 1 else "unnamed"
                current_section = HAProxySection(
                    type=SectionType.LISTEN,
                    name=name,
                    directives=[]
                )
                self.pos += 1

            else:
                # Directive within current section
                if current_section:
                    directive = self._parse_directive(line)
                    if directive:
                        current_section.directives.append(directive)
                self.pos += 1

        if current_section:
            sections.append(current_section)

        return sections

    def _preprocess(self, config_text: str) -> List[str]:
        """Preprocess config: remove comments."""
        lines = []

        for line in config_text.split('\n'):
            # Remove comments
            if '#' in line:
                line = line[:line.index('#')]

            line = line.strip()

            if line:
                lines.append(line)

        return lines

    def _parse_directive(self, line: str) -> Optional[Dict[str, Any]]:
        """Parse a single directive line."""
        parts = line.split(maxsplit=1)

        if not parts:
            return None

        directive_name = parts[0]
        directive_value = parts[1] if len(parts) > 1 else ""

        return {
            "name": directive_name,
            "value": directive_value
        }
```

### Provider.parse() Methode

```python
class HAProxyProvider(Provider):
    """HAProxy Provider with Import Support."""

    def parse(self, provider_config: str) -> Config:
        """Parse haproxy.cfg to GAL format.

        Args:
            provider_config: haproxy.cfg content

        Returns:
            Config: GAL configuration object

        Raises:
            ValueError: If config is invalid
        """
        parser = HAProxyConfigParser()
        sections = parser.parse(provider_config)

        self._import_warnings = []

        return Config(
            version="1.0",
            provider="haproxy",
            global_config=self._parse_global(sections),
            services=self._parse_services(sections)
        )

    def _parse_global(self, sections: List[HAProxySection]) -> GlobalConfig:
        """Extract global config from sections."""
        # Defaults section or first frontend bind
        return GlobalConfig(
            host="0.0.0.0",
            port=80,
            timeout="30s"
        )

    def _parse_services(self, sections: List[HAProxySection]) -> List[Service]:
        """Parse HAProxy backends to GAL services."""
        services = []

        # Collect backends
        backends = [s for s in sections if s.type == SectionType.BACKEND]

        # Collect frontends for routing info
        frontends = [s for s in sections if s.type == SectionType.FRONTEND]

        for backend in backends:
            service = self._parse_backend(backend, frontends)
            if service:
                services.append(service)

        return services

    def _parse_backend(
        self,
        backend: HAProxySection,
        frontends: List[HAProxySection]
    ) -> Optional[Service]:
        """Parse HAProxy backend to GAL service."""
        name = backend.name

        # Parse servers (targets)
        servers = []
        lb_algorithm = "round_robin"
        health_check = None
        sticky_sessions = False
        cookie_name = None

        for directive in backend.directives:
            if directive["name"] == "server":
                server = self._parse_server_directive(directive["value"])
                if server:
                    servers.append(server)

            elif directive["name"] == "balance":
                lb_algorithm = self._map_lb_algorithm(directive["value"])

            elif directive["name"] == "option":
                if "httpchk" in directive["value"]:
                    health_check = self._parse_httpchk(directive["value"])

            elif directive["name"] == "http-check":
                # HAProxy 2.0+ health check syntax
                if not health_check:
                    health_check = HealthCheckConfig()
                self._parse_http_check_directive(health_check, directive["value"])

            elif directive["name"] == "cookie":
                # Sticky sessions via cookie
                cookie_parts = directive["value"].split()
                if cookie_parts:
                    cookie_name = cookie_parts[0]
                    sticky_sessions = True

        # Build upstream
        targets = []
        for server in servers:
            targets.append(UpstreamTarget(
                host=server["host"],
                port=server["port"],
                weight=server.get("weight", 1)
            ))

        upstream = UpstreamConfig(
            targets=targets,
            load_balancer=LoadBalancerConfig(
                algorithm=lb_algorithm,
                sticky_sessions=sticky_sessions,
                cookie_name=cookie_name
            ),
            health_check=health_check
        )

        # Find routes from frontends
        routes = self._find_routes_for_backend(name, frontends)

        return Service(
            name=name,
            upstream=upstream,
            routes=routes
        )

    def _parse_server_directive(self, value: str) -> Optional[Dict[str, Any]]:
        """Parse HAProxy server directive.

        Format: server1 host:port [weight N] [check] [inter Xs] [fall N] [rise N]
        """
        parts = value.split()

        if len(parts) < 2:
            return None

        server_name = parts[0]
        address = parts[1]  # host:port

        # Parse host:port
        if ":" in address:
            host, port_str = address.rsplit(":", 1)
            port = int(port_str)
        else:
            host = address
            port = 80

        server = {
            "name": server_name,
            "host": host,
            "port": port
        }

        # Parse options
        i = 2
        while i < len(parts):
            if parts[i] == "weight" and i + 1 < len(parts):
                server["weight"] = int(parts[i + 1])
                i += 2
            elif parts[i] == "check":
                server["check"] = True
                i += 1
            elif parts[i] == "inter" and i + 1 < len(parts):
                server["inter"] = parts[i + 1]
                i += 2
            elif parts[i] == "fall" and i + 1 < len(parts):
                server["fall"] = int(parts[i + 1])
                i += 2
            elif parts[i] == "rise" and i + 1 < len(parts):
                server["rise"] = int(parts[i + 1])
                i += 2
            else:
                i += 1

        return server

    def _map_lb_algorithm(self, value: str) -> str:
        """Map HAProxy balance algorithm to GAL."""
        mapping = {
            "roundrobin": "round_robin",
            "leastconn": "least_conn",
            "source": "ip_hash",
            "uri": "ip_hash",  # Hash-based
            "hdr": "ip_hash"
        }

        for haproxy_algo, gal_algo in mapping.items():
            if value.startswith(haproxy_algo):
                return gal_algo

        return "round_robin"

    def _parse_httpchk(self, value: str) -> HealthCheckConfig:
        """Parse 'option httpchk' directive.

        Format: option httpchk [METHOD] [URI] [VERSION]
        """
        parts = value.split()

        method = "GET"
        uri = "/health"

        if len(parts) > 1:
            method = parts[1]
        if len(parts) > 2:
            uri = parts[2]

        return HealthCheckConfig(
            active=ActiveHealthCheck(
                enabled=True,
                http_path=uri,
                interval="10s",
                timeout="5s",
                healthy_threshold=2,
                unhealthy_threshold=3,
                healthy_status_codes=[200]
            )
        )

    def _parse_http_check_directive(self, health_check: HealthCheckConfig, value: str):
        """Parse 'http-check' directive (HAProxy 2.0+)."""
        if value.startswith("expect status"):
            # Extract status codes
            # Format: expect status 200-399
            status_range = value.split("status")[1].strip()

            # Simplified: just use 200
            if health_check.active:
                health_check.active.healthy_status_codes = [200]

    def _find_routes_for_backend(
        self,
        backend_name: str,
        frontends: List[HAProxySection]
    ) -> List[Route]:
        """Find routes that route to this backend."""
        routes = []

        for frontend in frontends:
            acls = {}
            use_backend_rules = []
            rate_limits = {}
            headers = {}
            cors_headers = {}

            for directive in frontend.directives:
                if directive["name"] == "acl":
                    # Parse ACL
                    acl = self._parse_acl_directive(directive["value"])
                    if acl:
                        acls[acl["name"]] = acl

                elif directive["name"] == "use_backend":
                    # Check if this routes to our backend
                    parts = directive["value"].split()
                    target_backend = parts[0] if parts else None

                    if target_backend == backend_name:
                        # Extract ACL conditions
                        acl_conditions = parts[1:] if len(parts) > 1 else []
                        use_backend_rules.append({
                            "backend": target_backend,
                            "acls": acl_conditions
                        })

                elif directive["name"] == "http-request":
                    # Rate limiting, headers, etc.
                    if "deny" in directive["value"] and "sc_http_req_rate" in directive["value"]:
                        # Rate limiting
                        rate_limit = self._parse_rate_limit_directive(directive["value"])
                        if rate_limit:
                            rate_limits["default"] = rate_limit

                    elif "set-header" in directive["value"]:
                        # Request headers
                        header = self._parse_set_header_directive(directive["value"])
                        if header:
                            headers[header["name"]] = header["value"]

                elif directive["name"] == "http-response":
                    # Response headers (could be CORS)
                    if "set-header" in directive["value"]:
                        header = self._parse_set_header_directive(directive["value"])
                        if header and header["name"].startswith("Access-Control-"):
                            cors_headers[header["name"]] = header["value"]

            # Build route for each use_backend rule
            for rule in use_backend_rules:
                # Find matching ACL to get path
                path_prefix = "/"
                for acl_name in rule["acls"]:
                    if acl_name.startswith("is_"):
                        acl = acls.get(acl_name)
                        if acl and acl.get("path"):
                            path_prefix = acl["path"]
                            break

                # Build route config
                route_config = {
                    "path_prefix": path_prefix
                }

                if rate_limits:
                    route_config["rate_limit"] = list(rate_limits.values())[0]

                if headers:
                    route_config["headers"] = HeadersConfig(
                        request_add=headers
                    )

                if cors_headers:
                    route_config["cors"] = self._build_cors_from_headers(cors_headers)

                routes.append(Route(**route_config))

        return routes

    def _parse_acl_directive(self, value: str) -> Optional[Dict[str, Any]]:
        """Parse ACL directive.

        Format: acl_name path_beg /api
        """
        parts = value.split(maxsplit=2)

        if len(parts) < 3:
            return None

        acl_name = parts[0]
        condition = parts[1]
        path_value = parts[2]

        acl = {
            "name": acl_name,
            "condition": condition
        }

        if condition == "path_beg":
            acl["path"] = path_value

        return acl

    def _parse_rate_limit_directive(self, value: str) -> RateLimitConfig:
        """Parse rate limit from http-request deny directive.

        Format: deny deny_status 429 if ... { sc_http_req_rate(0) gt 100 }
        """
        # Extract rate (simplified)
        import re
        match = re.search(r"gt (\d+)", value)

        rps = 100  # Default
        if match:
            rps = int(match.group(1))

        return RateLimitConfig(
            enabled=True,
            requests_per_second=rps,
            burst=rps * 2,
            key_type="ip_address"
        )

    def _parse_set_header_directive(self, value: str) -> Optional[Dict[str, str]]:
        """Parse http-request/http-response set-header directive.

        Format: set-header Header-Name "value"
        """
        # Extract header name and value
        import re
        match = re.search(r'set-header (\S+) "(.*?)"', value)

        if not match:
            match = re.search(r'set-header (\S+) (\S+)', value)

        if match:
            return {
                "name": match.group(1),
                "value": match.group(2)
            }

        return None

    def _build_cors_from_headers(self, cors_headers: Dict[str, str]) -> CorsConfig:
        """Build CORS config from Access-Control-* headers."""
        origins = cors_headers.get("Access-Control-Allow-Origin", "*").split(",")
        methods_str = cors_headers.get("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE")
        methods = methods_str.split(",")
        headers_str = cors_headers.get("Access-Control-Allow-Headers")
        headers_list = headers_str.split(",") if headers_str else None
        credentials = cors_headers.get("Access-Control-Allow-Credentials") == "true"
        max_age = cors_headers.get("Access-Control-Max-Age", "86400")

        return CorsConfig(
            enabled=True,
            allowed_origins=origins,
            allowed_methods=methods,
            allowed_headers=headers_list,
            allow_credentials=credentials,
            max_age=max_age
        )

    def get_import_warnings(self) -> List[str]:
        """Return warnings from last import."""
        return getattr(self, '_import_warnings', [])
```

## Feature Mapping Matrix

| GAL Feature | HAProxy Config | Mapping |
|-------------|---------------|---------|
| **Service** | `backend <name>` | Direct mapping |
| **Upstream Targets** | `server name host:port` | Direct mapping |
| **Load Balancing** | `balance roundrobin/leastconn/source` | Direct mapping |
| **Active Health Checks** | `option httpchk`, `http-check` | ✅ Full support |
| **Passive Health Checks** | `server ... fall N rise N` | ✅ Full support |
| **Sticky Sessions** | `cookie NAME insert` | ✅ Direct mapping |
| **Routes** | `use_backend` + ACLs | Extracted from ACL path_beg |
| **Rate Limiting** | `stick-table` + `http-request deny` | ⚠️ Complex stick-table syntax |
| **Headers** | `http-request set-header`, `http-response set-header` | Direct mapping |
| **CORS** | `http-response set-header Access-Control-*` | Extracted from headers |
| **Basic Auth** | `http-request auth` | ⚠️ Limited support |

## Beispiel-Konvertierung

### haproxy.cfg (Input)

```haproxy
global
    log 127.0.0.1 local0
    maxconn 4000

defaults
    mode http
    timeout connect 5s
    timeout client 30s
    timeout server 30s

frontend http_frontend
    bind 0.0.0.0:80

    acl is_api path_beg /api
    acl is_api_method method GET POST

    http-request track-sc0 src if is_api
    http-request deny deny_status 429 if is_api { sc_http_req_rate(0) gt 100 }

    http-request set-header X-Gateway "HAProxy" if is_api
    http-response set-header Access-Control-Allow-Origin "https://app.example.com" if is_api

    use_backend backend_api_service if is_api is_api_method

    stick-table type ip size 100k expire 30s store http_req_rate(10s)

backend backend_api_service
    balance leastconn

    cookie SERVERID insert indirect nocache

    option httpchk GET /health HTTP/1.1
    http-check expect status 200

    server server1 api-1.internal:8080 check inter 10s fall 3 rise 2 weight 2 cookie server1
    server server2 api-2.internal:8080 check inter 10s fall 3 rise 2 weight 1 cookie server2
```

### GAL Config (Output)

```yaml
version: "1.0"
provider: haproxy

global_config:
  host: 0.0.0.0
  port: 80
  timeout: 30s

services:
  - name: backend_api_service
    upstream:
      targets:
        - host: api-1.internal
          port: 8080
          weight: 2
        - host: api-2.internal
          port: 8080
          weight: 1

      load_balancer:
        algorithm: least_conn
        sticky_sessions: true
        cookie_name: SERVERID

      health_check:
        active:
          enabled: true
          http_path: /health
          interval: 10s
          timeout: 5s
          healthy_threshold: 2
          unhealthy_threshold: 3
          healthy_status_codes: [200]

    routes:
      - path_prefix: /api
        methods:
          - GET
          - POST

        rate_limit:
          enabled: true
          requests_per_second: 100
          burst: 200
          key_type: ip_address

        headers:
          request_add:
            X-Gateway: HAProxy

        cors:
          enabled: true
          allowed_origins:
            - https://app.example.com
```

## CLI Usage

```bash
# Import haproxy.cfg
gal import --provider haproxy --input /etc/haproxy/haproxy.cfg --output gal-config.yaml

# Generate for different provider
gal generate --config gal-config.yaml --provider envoy --output envoy.yaml
```

## Test Cases

30+ Tests:
- Parser Tests (sections, directives)
- Backend parsing
- Server directive parsing
- ACL parsing
- use_backend routing
- Balance algorithms
- Health checks (httpchk, http-check)
- Sticky sessions (cookie)
- Rate limiting (stick-table)
- Headers (http-request/http-response)
- CORS extraction
- Round-trip test

## Edge Cases

- **Stick-tables**: Komplex, vereinfachte Mapping
- **ACLs**: Vielfältig (path_beg, hdr, method, etc.)
- **Lua Scripts**: Nicht mappbar
- **TCP Mode**: Nur HTTP unterstützt

## Akzeptanzkriterien

- ✅ Custom Parser für haproxy.cfg
- ✅ Import von backends + frontends
- ✅ ACL → Route Mapping
- ✅ Sticky Sessions
- ✅ Advanced Health Checks
- ✅ Rate Limiting (stick-table based)
- ✅ Headers + CORS
- ✅ CLI Integration
- ✅ 30+ Tests, 85%+ Coverage
- ✅ Warnings für komplexe Features
- ✅ Round-trip Test

## Implementierungs-Reihenfolge

1. **Tag 1-3**: HAProxyConfigParser (Section Parser)
2. **Tag 4-5**: Backend + Server Parsing
3. **Tag 6-7**: Frontend + ACL Parsing
4. **Tag 8-9**: Rate Limiting + Headers
5. **Tag 10-12**: Sticky Sessions + Health Checks
6. **Tag 13-14**: Tests + Edge Cases + Documentation

## Nächste Schritte

Nach Completion:
1. Release als v1.3.0-beta2
2. User Feedback
3. Compatibility Checker (Feature 7) beginnen
