<!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>