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