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)
This commit is contained in:
parent
165f341946
commit
4e108c7d7f
1 changed files with 110 additions and 13 deletions
123
index.html
123
index.html
|
|
@ -697,6 +697,74 @@
|
||||||
const yamlStatus = document.getElementById('yamlStatus');
|
const yamlStatus = document.getElementById('yamlStatus');
|
||||||
let currentYaml = '';
|
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
|
// YAML validation function
|
||||||
function validateYAML(yamlText) {
|
function validateYAML(yamlText) {
|
||||||
yamlError.classList.remove('show');
|
yamlError.classList.remove('show');
|
||||||
|
|
@ -706,27 +774,56 @@
|
||||||
yamlStatus.classList.remove('valid', 'invalid');
|
yamlStatus.classList.remove('valid', 'invalid');
|
||||||
|
|
||||||
if (!yamlText || yamlText.trim() === '') {
|
if (!yamlText || yamlText.trim() === '') {
|
||||||
|
yamlStatus.textContent = 'YAML is empty';
|
||||||
|
yamlStatus.classList.add('invalid');
|
||||||
return { valid: false, error: 'YAML is empty' };
|
return { valid: false, error: 'YAML is empty' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let parsedConfig;
|
||||||
|
let yamlError = null;
|
||||||
|
|
||||||
|
// First, check YAML syntax
|
||||||
try {
|
try {
|
||||||
jsyaml.load(yamlText);
|
parsedConfig = 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 };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
yamlEditor.classList.add('error');
|
yamlEditor.classList.add('error');
|
||||||
yamlEditor.classList.remove('valid');
|
yamlEditor.classList.remove('valid');
|
||||||
yamlError.textContent = `YAML Error: ${error.message}`;
|
yamlError = error;
|
||||||
yamlError.classList.add('show');
|
|
||||||
yamlSuccess.classList.remove('show');
|
|
||||||
yamlStatus.textContent = '✗ Invalid YAML';
|
|
||||||
yamlStatus.classList.add('invalid');
|
|
||||||
return { valid: false, error: error.message };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Real-time YAML linting with debounce
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue