From 4e108c7d7fdfa2e2c921e1e7f425e3e1882be049 Mon Sep 17 00:00:00 2001 From: robojerk Date: Fri, 31 Oct 2025 12:03:31 -0700 Subject: [PATCH] Add Docker Compose structure validation to YAML editor - Enhanced validation beyond YAML syntax - Checks for required Docker Compose structure (services, volumes) - Validates volume references match defined volumes - Validates port formats - Provides clear error messages for Docker Compose issues - Keeps js-yaml for syntax validation (simple and fast) --- index.html | 123 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 110 insertions(+), 13 deletions(-) diff --git a/index.html b/index.html index 5de9ecd..59a77d7 100644 --- a/index.html +++ b/index.html @@ -697,6 +697,74 @@ const yamlStatus = document.getElementById('yamlStatus'); let currentYaml = ''; + // Docker Compose structure validation + function validateDockerCompose(config) { + const errors = []; + + // Check for required top-level keys + if (!config.services) { + errors.push('Missing "services" section'); + return errors; + } + + // Check services + const serviceNames = Object.keys(config.services); + if (serviceNames.length === 0) { + errors.push('No services defined'); + } + + // Validate each service + serviceNames.forEach(serviceName => { + const service = config.services[serviceName]; + + // Check for required service fields + if (!service.image && !service.build) { + errors.push(`Service "${serviceName}": Missing "image" or "build"`); + } + + // Check volume references match defined volumes + if (service.volumes && Array.isArray(service.volumes)) { + service.volumes.forEach(volume => { + // Extract volume name from volume string (format: "volume_name:/path" or "/host:/path") + if (typeof volume === 'string') { + const parts = volume.split(':'); + const volumeName = parts[0]; + + // Check if it's a named volume (not a bind mount starting with / or ./) + if (volumeName && !volumeName.startsWith('/') && !volumeName.startsWith('./') && volumeName !== '~') { + // Check if volume is defined + if (config.volumes && !config.volumes.hasOwnProperty(volumeName)) { + errors.push(`Service "${serviceName}": Volume "${volumeName}" is referenced but not defined in "volumes" section`); + } + } + } + }); + } + + // Validate port format + if (service.ports && Array.isArray(service.ports)) { + service.ports.forEach(port => { + if (typeof port === 'string') { + // Basic port format check: "host:container" or just number + const parts = port.split(':'); + if (parts.length === 2 && (!parts[0].match(/^\d+$/) || !parts[1].match(/^\d+\/?\w*$/))) { + errors.push(`Service "${serviceName}": Invalid port format "${port}" (expected "host:container")`); + } + } + }); + } + + // Check environment variables format + if (service.environment) { + if (typeof service.environment !== 'object') { + errors.push(`Service "${serviceName}": "environment" must be an object or array`); + } + } + }); + + return errors; + } + // YAML validation function function validateYAML(yamlText) { yamlError.classList.remove('show'); @@ -706,27 +774,56 @@ yamlStatus.classList.remove('valid', 'invalid'); if (!yamlText || yamlText.trim() === '') { + yamlStatus.textContent = 'YAML is empty'; + yamlStatus.classList.add('invalid'); return { valid: false, error: 'YAML is empty' }; } + let parsedConfig; + let yamlError = null; + + // First, check YAML syntax try { - jsyaml.load(yamlText); - yamlEditor.classList.add('valid'); - yamlError.classList.remove('show'); - yamlSuccess.classList.add('show'); - yamlStatus.textContent = '✓ Valid YAML'; - yamlStatus.classList.add('valid'); - return { valid: true }; + parsedConfig = jsyaml.load(yamlText); } catch (error) { yamlEditor.classList.add('error'); yamlEditor.classList.remove('valid'); - yamlError.textContent = `YAML Error: ${error.message}`; - yamlError.classList.add('show'); - yamlSuccess.classList.remove('show'); - yamlStatus.textContent = '✗ Invalid YAML'; - yamlStatus.classList.add('invalid'); - return { valid: false, error: error.message }; + yamlError = error; } + + if (yamlError) { + // YAML syntax error + document.getElementById('yamlError').textContent = `YAML Syntax Error: ${yamlError.message}`; + document.getElementById('yamlError').classList.add('show'); + yamlSuccess.classList.remove('show'); + yamlStatus.textContent = '✗ Invalid YAML Syntax'; + yamlStatus.classList.add('invalid'); + return { valid: false, error: yamlError.message }; + } + + // YAML is syntactically valid, now check Docker Compose structure + const composeErrors = validateDockerCompose(parsedConfig); + + if (composeErrors.length > 0) { + // Docker Compose structure errors + yamlEditor.classList.add('error'); + yamlEditor.classList.remove('valid'); + document.getElementById('yamlError').textContent = `Docker Compose Errors:\n• ${composeErrors.join('\n• ')}`; + document.getElementById('yamlError').classList.add('show'); + yamlSuccess.classList.remove('show'); + yamlStatus.textContent = '✗ Docker Compose Validation Failed'; + yamlStatus.classList.add('invalid'); + return { valid: false, error: composeErrors.join('; ') }; + } + + // All valid! + yamlEditor.classList.add('valid'); + yamlEditor.classList.remove('error'); + document.getElementById('yamlError').classList.remove('show'); + yamlSuccess.classList.add('show'); + yamlStatus.textContent = '✓ Valid Docker Compose YAML'; + yamlStatus.classList.add('valid'); + return { valid: true }; } // Real-time YAML linting with debounce