Extract YAML validation to separate module
- Created yaml-validator.js with pure validation functions - Removed redundant checkYAMLFormatting function (rely on js-yaml) - Fixed function shadowing issues by using window._validateYAMLPure - Updated nginx config to serve JS files with correct MIME type - Improved error reporting with line/column numbers from js-yaml
This commit is contained in:
parent
4e108c7d7f
commit
8d2def360e
4 changed files with 201 additions and 116 deletions
12
Dockerfile
12
Dockerfile
|
|
@ -18,7 +18,7 @@ RUN pip install --no-cache-dir -r backend/requirements.txt
|
||||||
COPY backend/ /app/backend/
|
COPY backend/ /app/backend/
|
||||||
|
|
||||||
# Copy frontend files
|
# Copy frontend files
|
||||||
COPY index.html manage.html /usr/share/nginx/html/
|
COPY index.html manage.html yaml-validator.js /usr/share/nginx/html/
|
||||||
|
|
||||||
# Configure nginx
|
# Configure nginx
|
||||||
RUN echo 'server {\n\
|
RUN echo 'server {\n\
|
||||||
|
|
@ -27,8 +27,10 @@ RUN echo 'server {\n\
|
||||||
root /usr/share/nginx/html;\n\
|
root /usr/share/nginx/html;\n\
|
||||||
index index.html;\n\
|
index index.html;\n\
|
||||||
\n\
|
\n\
|
||||||
location / {\n\
|
# Serve JavaScript files with correct MIME type (must come before location /)\n\
|
||||||
try_files $uri $uri/ /index.html;\n\
|
location ~* \\.js$ {\n\
|
||||||
|
default_type application/javascript;\n\
|
||||||
|
try_files $uri =404;\n\
|
||||||
}\n\
|
}\n\
|
||||||
\n\
|
\n\
|
||||||
location /api {\n\
|
location /api {\n\
|
||||||
|
|
@ -36,6 +38,10 @@ RUN echo 'server {\n\
|
||||||
proxy_set_header Host $host;\n\
|
proxy_set_header Host $host;\n\
|
||||||
proxy_set_header X-Real-IP $remote_addr;\n\
|
proxy_set_header X-Real-IP $remote_addr;\n\
|
||||||
}\n\
|
}\n\
|
||||||
|
\n\
|
||||||
|
location / {\n\
|
||||||
|
try_files $uri $uri/ /index.html;\n\
|
||||||
|
}\n\
|
||||||
}' > /etc/nginx/sites-available/default
|
}' > /etc/nginx/sites-available/default
|
||||||
|
|
||||||
# Configure supervisor
|
# Configure supervisor
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ services:
|
||||||
# Hot reload frontend (optional, for development)
|
# Hot reload frontend (optional, for development)
|
||||||
- ./index.html:/usr/share/nginx/html/index.html:ro,z
|
- ./index.html:/usr/share/nginx/html/index.html:ro,z
|
||||||
- ./manage.html:/usr/share/nginx/html/manage.html:ro,z
|
- ./manage.html:/usr/share/nginx/html/manage.html:ro,z
|
||||||
|
- ./yaml-validator.js:/usr/share/nginx/html/yaml-validator.js:ro,z
|
||||||
environment:
|
environment:
|
||||||
# Set working directory for database
|
# Set working directory for database
|
||||||
- PWD=/app/data
|
- PWD=/app/data
|
||||||
|
|
|
||||||
163
index.html
163
index.html
|
|
@ -5,6 +5,7 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Stronghold - Minecraft Server Generator</title>
|
<title>Stronghold - Minecraft Server Generator</title>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js"></script>
|
||||||
|
<script src="yaml-validator.js"></script>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -697,138 +698,78 @@
|
||||||
const yamlStatus = document.getElementById('yamlStatus');
|
const yamlStatus = document.getElementById('yamlStatus');
|
||||||
let currentYaml = '';
|
let currentYaml = '';
|
||||||
|
|
||||||
// Docker Compose structure validation
|
// YAML validation function (wrapper that handles DOM updates)
|
||||||
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) {
|
function validateYAML(yamlText) {
|
||||||
yamlError.classList.remove('show');
|
const errorElement = document.getElementById('yamlError');
|
||||||
yamlSuccess.classList.remove('show');
|
const successElement = document.getElementById('yamlSuccess');
|
||||||
|
|
||||||
|
errorElement.classList.remove('show');
|
||||||
|
successElement.classList.remove('show');
|
||||||
yamlEditor.classList.remove('error', 'valid');
|
yamlEditor.classList.remove('error', 'valid');
|
||||||
yamlStatus.textContent = '';
|
yamlStatus.textContent = '';
|
||||||
yamlStatus.classList.remove('valid', 'invalid');
|
yamlStatus.classList.remove('valid', 'invalid');
|
||||||
|
|
||||||
if (!yamlText || yamlText.trim() === '') {
|
// Get the pure validation function from yaml-validator.js
|
||||||
yamlStatus.textContent = 'YAML is empty';
|
// yaml-validator.js exposes it as window._validateYAMLPure to avoid shadowing
|
||||||
|
const pureValidateFn = (typeof window !== 'undefined' && window._validateYAMLPure)
|
||||||
|
? window._validateYAMLPure
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!pureValidateFn) {
|
||||||
|
errorElement.textContent = 'Error: YAML validator library not loaded. Please refresh the page.';
|
||||||
|
errorElement.classList.add('show');
|
||||||
|
yamlStatus.textContent = '✗ Validation Error';
|
||||||
yamlStatus.classList.add('invalid');
|
yamlStatus.classList.add('invalid');
|
||||||
return { valid: false, error: 'YAML is empty' };
|
console.error('Cannot access _validateYAMLPure from yaml-validator.js');
|
||||||
|
return { valid: false, error: 'YAML validator not loaded', config: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsedConfig;
|
// Call the pure function from yaml-validator.js
|
||||||
let yamlError = null;
|
const result = pureValidateFn(yamlText);
|
||||||
|
|
||||||
// First, check YAML syntax
|
if (!result.valid) {
|
||||||
try {
|
// Show error
|
||||||
parsedConfig = jsyaml.load(yamlText);
|
errorElement.textContent = result.error;
|
||||||
} catch (error) {
|
errorElement.classList.add('show');
|
||||||
|
successElement.classList.remove('show');
|
||||||
yamlEditor.classList.add('error');
|
yamlEditor.classList.add('error');
|
||||||
yamlEditor.classList.remove('valid');
|
yamlEditor.classList.remove('valid');
|
||||||
yamlError = error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (yamlError) {
|
// Determine status text based on error type
|
||||||
// YAML syntax error
|
if (result.error.includes('Syntax Error')) {
|
||||||
document.getElementById('yamlError').textContent = `YAML Syntax Error: ${yamlError.message}`;
|
yamlStatus.textContent = '✗ Invalid YAML Syntax';
|
||||||
document.getElementById('yamlError').classList.add('show');
|
} else if (result.error.includes('Docker Compose')) {
|
||||||
yamlSuccess.classList.remove('show');
|
yamlStatus.textContent = '✗ Docker Compose Validation Failed';
|
||||||
yamlStatus.textContent = '✗ Invalid YAML Syntax';
|
} else {
|
||||||
|
yamlStatus.textContent = '✗ Validation Error';
|
||||||
|
}
|
||||||
yamlStatus.classList.add('invalid');
|
yamlStatus.classList.add('invalid');
|
||||||
return { valid: false, error: yamlError.message };
|
} else {
|
||||||
|
// All valid!
|
||||||
|
yamlEditor.classList.add('valid');
|
||||||
|
yamlEditor.classList.remove('error');
|
||||||
|
errorElement.classList.remove('show');
|
||||||
|
successElement.classList.add('show');
|
||||||
|
yamlStatus.textContent = '✓ Valid Docker Compose YAML';
|
||||||
|
yamlStatus.classList.add('valid');
|
||||||
}
|
}
|
||||||
|
|
||||||
// YAML is syntactically valid, now check Docker Compose structure
|
return result;
|
||||||
const composeErrors = validateDockerCompose(parsedConfig);
|
}
|
||||||
|
|
||||||
if (composeErrors.length > 0) {
|
// Check if js-yaml is loaded
|
||||||
// Docker Compose structure errors
|
if (typeof jsyaml === 'undefined') {
|
||||||
yamlEditor.classList.add('error');
|
console.error('js-yaml library not loaded!');
|
||||||
yamlEditor.classList.remove('valid');
|
yamlStatus.textContent = 'Error: YAML library not loaded';
|
||||||
document.getElementById('yamlError').textContent = `Docker Compose Errors:\n• ${composeErrors.join('\n• ')}`;
|
yamlStatus.classList.add('invalid');
|
||||||
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
|
||||||
let lintTimeout;
|
let lintTimeout;
|
||||||
yamlEditor.addEventListener('input', () => {
|
yamlEditor.addEventListener('input', () => {
|
||||||
|
if (typeof jsyaml === 'undefined') {
|
||||||
|
return; // Don't validate if library isn't loaded
|
||||||
|
}
|
||||||
clearTimeout(lintTimeout);
|
clearTimeout(lintTimeout);
|
||||||
lintTimeout = setTimeout(() => {
|
lintTimeout = setTimeout(() => {
|
||||||
validateYAML(yamlEditor.value);
|
validateYAML(yamlEditor.value);
|
||||||
|
|
|
||||||
137
yaml-validator.js
Normal file
137
yaml-validator.js
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
/**
|
||||||
|
* YAML Linting and Docker Compose Validation
|
||||||
|
*
|
||||||
|
* Pure validation functions for YAML syntax, formatting, and Docker Compose structure.
|
||||||
|
* Requires js-yaml library to be loaded globally.
|
||||||
|
*
|
||||||
|
* Note: We rely on js-yaml for ALL syntax and formatting validation, including indentation.
|
||||||
|
* The js-yaml parser is comprehensive and handles all YAML edge cases correctly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main YAML validation function (pure function, no DOM manipulation)
|
||||||
|
* @param {string} yamlText - The YAML text to validate
|
||||||
|
* @returns {Object} Validation result with {valid: boolean, error: string|null, config: Object|null}
|
||||||
|
*/
|
||||||
|
function validateYAMLPure(yamlText) {
|
||||||
|
// Check if js-yaml is available
|
||||||
|
if (typeof jsyaml === 'undefined') {
|
||||||
|
return { valid: false, error: 'YAML validation library not loaded', config: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!yamlText || yamlText.trim() === '') {
|
||||||
|
return { valid: true, error: null, config: null }; // Treat empty as valid (no errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedConfig;
|
||||||
|
|
||||||
|
// 1. Rely on js-yaml for ALL syntax/formatting checks (Indentation, spacing, etc.)
|
||||||
|
try {
|
||||||
|
parsedConfig = jsyaml.load(yamlText);
|
||||||
|
} catch (error) {
|
||||||
|
// Extract precise location data from the js-yaml error object
|
||||||
|
const line = error.mark ? error.mark.line + 1 : 'Unknown';
|
||||||
|
const column = error.mark ? error.mark.column + 1 : 'Unknown';
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `YAML Syntax Error (Line ${line}, Col ${column}): ${error.reason}`,
|
||||||
|
config: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// A valid YAML parse might return null or undefined for an empty file
|
||||||
|
if (!parsedConfig) {
|
||||||
|
return { valid: true, error: null, config: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. YAML is syntactically valid, now check Docker Compose structure
|
||||||
|
const composeErrors = validateDockerCompose(parsedConfig);
|
||||||
|
|
||||||
|
if (composeErrors.length > 0) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `Docker Compose Structural Errors:\n• ${composeErrors.join('\n• ')}`,
|
||||||
|
config: parsedConfig
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// All valid!
|
||||||
|
return { valid: true, error: 'YAML is valid and meets basic Docker Compose requirements.', config: parsedConfig };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose the function on window with a unique name to avoid conflicts
|
||||||
|
// This must happen at the end of the script, after the function is defined
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window._validateYAMLPure = validateYAMLPure;
|
||||||
|
console.log('Exposed _validateYAMLPure on window');
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue