Newer
Older
EnvoyControlPlane / static / filter_chain.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Envoy Filter Chain Configurator</title>
    <!-- Load Tailwind CSS for modern styling -->
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        /* Custom styles for better readability and structure */
        body { font-family: "Inter", sans-serif; background-color: #f7f7f7; }
        #app-container { max-width: 900px; }
        /* Style for the YAML output area (now a textarea) */
        #yaml-output {
            background-color: #1e1e1e;
            color: #d4d4d4;
            border: 1px solid #444;
            font-family: 'Consolas', 'Monaco', monospace;
            line-height: 1.4;
            min-height: 400px; 
            padding: 15px;
            box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
        }
        #yaml-output:disabled {
            cursor: default;
        }

        .optional-fields {
            transition: all 0.3s ease-in-out;
            border-left: 4px solid #3b82f6;
        }
        
        /* Disable styling for Simple Mode inputs when Advanced Mode is on */
        .simple-disabled {
            opacity: 0.6;
            pointer-events: none;
            transition: opacity 0.3s ease;
        }

        /* Custom toggle switch styling for Advanced Mode */
        #advanced-mode-toggle:checked ~ .block {
            background-color: #10b981; /* Green color when checked */
        }
        #advanced-mode-toggle:checked ~ .dot {
            transform: translateX(100%);
        }

        /* Hide the button since generation is now real-time */
        #generate-button {
            display: none; 
        }
        /* Style adjustments for better form readability */
        input[type="text"], textarea { 
            transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 
        }
    </style>
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    borderRadius: {
                        'xl': '0.75rem',
                    }
                }
            }
        }
    </script>
</head>
<body class="p-4 sm:p-8">
    <div id="app-container" class="mx-auto bg-white shadow-2xl rounded-xl p-6 md:p-10">
        <h1 class="text-3xl font-extrabold text-gray-800 mb-6 border-b pb-2">Envoy Filter Chain Composer</h1>
        <p class="text-gray-600 mb-8">Configure your Envoy listener filter chain using simple inputs or switch to advanced editing mode.</p>

        <div class="space-y-6" id="simple-mode-inputs">
            <!-- Domain & Service Name -->
            <div class="grid md:grid-cols-2 gap-6">
                <div>
                    <label for="domain" class="block text-sm font-medium text-gray-700">Service Domain (e.g., api.example.com):</label>
                    <input type="text" id="domain" value="pcb.jerxie.com" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 p-3 border">
                </div>
                <div>
                    <label for="service-name" class="block text-sm font-medium text-gray-700">Cluster Name (e.g., _pcb_server):</label>
                    <input type="text" id="service-name" value="_pcb_server" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 p-3 border">
                </div>
            </div>

            <!-- Route Configuration -->
            <div>
                <label for="route-config" class="block text-sm font-medium text-gray-700">Route Configuration YAML (The text 'CLUSTER_PLACEHOLDER' will be replaced by the Cluster Name):</label>
                <!-- NOTE: The default route config is indented 2 spaces so the final calculation is simpler in JS -->
                <textarea id="route-config" rows="6" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 p-3 border font-mono text-sm">
  - match:
      prefix: /
    route:
      cluster: CLUSTER_PLACEHOLDER
      timeout: 0s</textarea>
            </div>

            <!-- Options Checkboxes -->
            <div class="flex flex-col sm:flex-row gap-6 p-4 bg-blue-50 rounded-lg">
                <div class="flex items-center">
                    <input id="enable-websocket" type="checkbox" class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
                    <label for="enable-websocket" class="ml-2 block text-sm font-medium text-gray-700 select-none">Enable WebSocket Upgrade</label>
                </div>
                <div class="flex items-center">
                    <input id="enable-tls" type="checkbox" class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
                    <label for="enable-tls" class="ml-2 block text-sm font-medium text-gray-700 select-none">Enable TLS (HTTPS)</label>
                </div>
            </div>

            <!-- TLS Optional Fields -->
            <div id="tls-fields" class="optional-fields p-4 bg-gray-50 rounded-lg hidden">
                <h3 class="text-lg font-semibold text-gray-800 mb-4">TLS Certificate Paths</h3>
                <p class="text-xs text-gray-500 mb-4">The 'DOMAIN_PLACEHOLDER' will be replaced by your Service Domain.</p>
                
                <div class="space-y-4">
                    <div>
                        <label for="cert-chain" class="block text-sm font-medium text-gray-700">Certificate Chain Path:</label>
                        <input type="text" id="cert-chain" value="/etc/certs/downstream/DOMAIN_PLACEHOLDER/fullchain.pem" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm p-2 border text-sm">
                    </div>
                    <div>
                        <label for="private-key" class="block text-sm font-medium text-gray-700">Private Key Path:</label>
                        <input type="text" id="private-key" value="/etc/certs/downstream/DOMAIN_PLACEHOLDER/privkey.pem" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm p-2 border text-sm">
                    </div>
                </div>
            </div>
        </div>

        <!-- Advanced Mode Toggle -->
        <div class="mt-8 flex items-center justify-between p-4 bg-gray-100 rounded-xl shadow-inner">
            <h2 class="text-xl font-bold text-gray-800">Advanced Editing Mode</h2>
            <label for="advanced-mode-toggle" class="flex items-center cursor-pointer select-none">
                <span class="text-sm font-medium text-gray-600 mr-3">Manual YAML Override</span>
                <div class="relative">
                    <input type="checkbox" id="advanced-mode-toggle" class="sr-only">
                    <div class="block bg-gray-600 w-14 h-8 rounded-full transition duration-300"></div>
                    <div class="dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition transform duration-300"></div>
                </div>
            </label>
        </div>

        <!-- YAML Output Area (Now a textarea) -->
        <h2 class="text-2xl font-bold text-gray-800 mt-10 mb-4">Composed YAML Section</h2>
        <textarea id="yaml-output" rows="25" class="mt-1 block w-full rounded-xl border-gray-300 shadow-lg p-4 font-mono text-sm resize-none"></textarea>
    </div>

    <script>
        // Global flag and cache for simple mode inputs
        let isAdvancedMode = false;
        let simpleInputs = []; 

        // --- Core YAML Generation Logic ---
        function generateYaml() {
            // Stop automatic generation if user is manually editing
            if (isAdvancedMode) {
                return;
            }

            const domain = document.getElementById('domain').value.trim();
            const serviceName = document.getElementById('service-name').value.trim();
            const rawRouteConfig = document.getElementById('route-config').value; // Keep whitespace for indentation calculation
            const enableWebsocket = document.getElementById('enable-websocket').checked;
            const enableTls = document.getElementById('enable-tls').checked;
            const certChain = document.getElementById('cert-chain').value.trim();
            const privateKey = document.getElementById('private-key').value.trim();
            const outputElement = document.getElementById('yaml-output');
            
            outputElement.value = 'Generating...';

            if (!domain || !serviceName) {
                outputElement.value = 'ERROR: Domain and Cluster Name are required.';
                return;
            }
            
            // --- 1. Base YAML Structure (HttpConnectionManager) ---
            // FIX: Corrected indentation for serverNames, filters, and all subsequent blocks 
            // to maintain a consistent 2-space indentation style relative to the parent list item (-).
            let yaml = `  - filterChainMatch:
      serverNames:
        - ${domain}
    filters:
      - name: envoy.filters.network.http_connection_manager
        typedConfig:
          '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          httpFilters:
            - name: envoy.filters.http.router
              typedConfig:
                '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
          routeConfig:
            virtualHosts:
              - domains:
                  - ${domain}
                # Use a sanitised version of the service name for the virtual host name
                name: ${serviceName.replace(/[^a-zA-Z0-9_]/g, '').toLowerCase()}_host
                routes:
# ROUTE CONFIGURATION START
`;
            
            // --- 2. Inject Route Configuration with proper indentation and substitution ---
            // The routes: key is at 14 spaces. 
            // The raw route config starts with 2 spaces ('  - match:').
            // Target start position: 16 spaces (14 + 2).
            // We need a prefix of 14 spaces (16 - 2 spaces from the raw config's start)
            
            // FIX: Updated prefix to 14 spaces for correct route alignment
            const routeIndentationPrefix = '              '; 
            
            const routesWithSubstitution = rawRouteConfig
                .replace(/CLUSTER_PLACEHOLDER/g, serviceName)
                .trimStart() 
                .replace(/^/gm, routeIndentationPrefix);
                
            yaml += routesWithSubstitution;
            
            yaml += `
# ROUTE CONFIGURATION END
          statPrefix: ingress_http`;

            // --- 3. WebSocket Configuration (Optional) ---
            if (enableWebsocket) {
                yaml += `
          upgradeConfigs:
            - upgradeType: websocket`;
            }
            
            // --- 4. TLS Configuration (Optional) ---
            if (enableTls) {
                const resolvedCertChain = certChain.replace(/DOMAIN_PLACEHOLDER/g, domain);
                const resolvedPrivateKey = privateKey.replace(/DOMAIN_PLACEHOLDER/g, domain);

                if (!resolvedCertChain || !resolvedPrivateKey) {
                    outputElement.value = 'ERROR: TLS is enabled, but Certificate Chain and Private Key paths are required.';
                    return;
                }
                
                // FIX: Aligned transportSocket correctly (4 spaces total)
                yaml += `
    transportSocket:
      name: envoy.transport_sockets.tls
      typedConfig:
        '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
        commonTlsContext:
          tlsCertificates:
            - certificateChain:
                filename: ${resolvedCertChain}
              privateKey:
                filename: ${resolvedPrivateKey}`;
            }

            // Final Output
            outputElement.value = yaml.trim();
        }


        // --- Advanced Mode Toggle Logic ---
        function toggleAdvancedMode(isTogglingOn) {
            const yamlOutput = document.getElementById('yaml-output');
            const simpleInputsContainer = document.getElementById('simple-mode-inputs');
            
            isAdvancedMode = isTogglingOn;

            if (isTogglingOn) {
                // SWITCHING TO ADVANCED MODE (Editable Output, Disabled Inputs)
                
                // 1. Disable simple inputs visually and functionally
                simpleInputsContainer.classList.add('simple-disabled');
                simpleInputs.forEach(input => { input.disabled = true; });
                
                // 2. Enable the YAML output area for editing
                yamlOutput.disabled = false;
                yamlOutput.focus();

            } else {
                // SWITCHING TO SIMPLE MODE (Read-only Output, Enabled Inputs)
                
                // 1. Enable simple inputs visually and functionally
                simpleInputsContainer.classList.remove('simple-disabled');
                simpleInputs.forEach(input => { input.disabled = false; });
                
                // 2. Disable the YAML output area (it becomes the display)
                yamlOutput.disabled = true;
                
                // 3. IMPORTANT: Regenerate YAML immediately to overwrite any manual changes
                generateYaml(); 
            }
        }


        // --- Event Listeners for UI interaction ---
        document.addEventListener('DOMContentLoaded', function() {
            const advancedModeToggle = document.getElementById('advanced-mode-toggle');
            const tlsFields = document.getElementById('tls-fields');

            // Cache all inputs in the simple mode container
            const domainInput = document.getElementById('domain');
            const serviceNameInput = document.getElementById('service-name');
            const routeConfigTextarea = document.getElementById('route-config');
            const enableWebsocketCheckbox = document.getElementById('enable-websocket');
            const enableTlsCheckbox = document.getElementById('enable-tls');
            const certChainInput = document.getElementById('cert-chain');
            const privateKeyInput = document.getElementById('private-key');

            // Populate the global simpleInputs array
            simpleInputs = [
                domainInput, serviceNameInput, routeConfigTextarea, 
                enableWebsocketCheckbox, enableTlsCheckbox, 
                certChainInput, privateKeyInput
            ];

            // 1. Attach generateYaml to appropriate events for real-time updates
            simpleInputs.forEach(input => {
                const eventType = (input.type === 'checkbox' || input.tagName === 'SELECT' || input.id === 'route-config') ? 'change' : 'input';
                input.addEventListener(eventType, generateYaml);
            });
            
            // 2. Advanced Mode Toggle Listener
            advancedModeToggle.addEventListener('change', function() {
                toggleAdvancedMode(this.checked);
            });

            // 3. TLS Fields Visibility Listener (also regenerates via its inputs)
            enableTlsCheckbox.addEventListener('change', function() {
                tlsFields.classList.toggle('hidden', !this.checked);
            });

            // 4. Update domain placeholders in TLS paths
            const updateTlsPaths = () => {
                const newDomain = domainInput.value.trim();
                document.getElementById('cert-chain').value = `/etc/certs/downstream/${newDomain}/fullchain.pem`;
                document.getElementById('private-key').value = `/etc/certs/downstream/${newDomain}/privkey.pem`;
            };
            
            domainInput.addEventListener('input', updateTlsPaths);
            
            // 5. Initial Setup: Simple Mode is default
            updateTlsPaths();
            toggleAdvancedMode(false); // Initialize in Simple Mode
        });
    </script>
</body>
</html>