UNPKG

docker-pilot

Version:

A powerful, scalable Docker CLI library for managing containerized applications of any size

548 lines 21.1 kB
"use strict"; /** * Validation utility functions * Provides validation for various Docker Pilot configurations and inputs */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ValidationUtils = void 0; const zod_1 = require("zod"); const semver = __importStar(require("semver")); const types_1 = require("../types"); const FileUtils_1 = require("./FileUtils"); const i18n_1 = require("./i18n"); class ValidationUtils { constructor(_logger, fileUtils) { this.fileUtils = fileUtils || new FileUtils_1.FileUtils(); this.i18n = new i18n_1.I18n(); } /** * Update language for validation messages */ updateLanguage(language) { this.i18n.setLanguage(language); } /** * Validate Docker Pilot configuration */ async validateConfig(config) { const result = { valid: true, errors: [], warnings: [] }; try { // Schema validation using Zod const validatedConfig = types_1.DockerPilotConfigSchema.parse(config); // Additional business logic validations await this.validateBusinessRules(validatedConfig, result); } catch (error) { if (error instanceof zod_1.z.ZodError) { result.valid = false; result.errors = error.errors.map(err => ({ field: err.path.join('.'), message: this.i18n.t('validation.config_invalid') + ': ' + err.message, code: err.code, value: 'received' in err ? err.received : undefined })); } else { result.valid = false; result.errors.push({ field: 'general', message: this.i18n.t('validation.config_invalid'), code: 'UNKNOWN_ERROR' }); } } return result; } /** * Validate business rules beyond schema validation */ async validateBusinessRules(config, result) { // Validate project name this.validateProjectName(config.projectName, result); // Validate services for (const [serviceName, serviceConfig] of Object.entries(config.services)) { await this.validateService(serviceName, serviceConfig, result); } // Validate Docker Compose configuration this.validateDockerComposeCommand(config.dockerCompose, result); // Validate plugin paths await this.validatePlugins(config.plugins, result); // Validate backup configuration this.validateBackupConfig(config.backup, result); // Validate monitoring configuration this.validateMonitoringConfig(config.monitoring, result); // Check for common misconfigurations this.checkCommonMisconfigurations(config, result); } /** * Validate project name */ validateProjectName(projectName, result) { // Check for valid Docker project name const dockerProjectNameRegex = /^[a-z0-9][a-z0-9_-]*$/; if (!dockerProjectNameRegex.test(projectName.toLowerCase())) { result.warnings.push({ field: 'projectName', message: 'Project name should contain only lowercase letters, numbers, hyphens, and underscores', suggestion: 'Use a Docker-compatible project name format' }); } if (projectName.length > 63) { result.errors.push({ field: 'projectName', message: 'Project name cannot exceed 63 characters', code: 'INVALID_LENGTH', value: projectName }); result.valid = false; } } /** * Validate individual service configuration */ async validateService(serviceName, serviceConfig, result) { // Validate service name if (!this.isValidServiceName(serviceName)) { result.errors.push({ field: `services.${serviceName}`, message: 'Invalid service name format', code: 'INVALID_SERVICE_NAME', value: serviceName }); result.valid = false; } // Validate port configuration if (serviceConfig.port !== null && serviceConfig.port !== undefined) { if (!this.isValidPort(serviceConfig.port)) { result.errors.push({ field: `services.${serviceName}.port`, message: 'Port must be between 1 and 65535', code: 'INVALID_PORT', value: serviceConfig.port }); result.valid = false; } // Check for common port conflicts this.checkPortConflicts(serviceName, serviceConfig.port, result); } // Validate path if provided if (serviceConfig.path) { const pathExists = await this.fileUtils.exists(serviceConfig.path); if (!pathExists) { result.warnings.push({ field: `services.${serviceName}.path`, message: `Service path does not exist: ${serviceConfig.path}`, suggestion: 'Ensure the path exists or remove it from configuration' }); } } // Validate environment variables if (serviceConfig.environment) { this.validateEnvironmentVariables(serviceName, serviceConfig.environment, result); } // Validate volume mounts if (serviceConfig.volumes) { this.validateVolumes(serviceName, serviceConfig.volumes, result); } // Validate resource limits if (serviceConfig.cpu_limit) { this.validateCpuLimit(serviceName, serviceConfig.cpu_limit, result); } if (serviceConfig.memory_limit) { this.validateMemoryLimit(serviceName, serviceConfig.memory_limit, result); } } /** * Validate Docker Compose command */ validateDockerComposeCommand(dockerCompose, result) { const validCommands = ['docker-compose', 'docker compose']; if (!validCommands.includes(dockerCompose)) { result.warnings.push({ field: 'dockerCompose', message: `Non-standard Docker Compose command: ${dockerCompose}`, suggestion: 'Consider using "docker compose" (newer) or "docker-compose" (legacy)' }); } } /** * Validate plugin paths */ async validatePlugins(plugins, result) { for (const pluginPath of plugins) { const exists = await this.fileUtils.exists(pluginPath); if (!exists) { result.warnings.push({ field: 'plugins', message: `Plugin file not found: ${pluginPath}`, suggestion: 'Ensure plugin files exist or remove them from configuration' }); } } } /** * Validate backup configuration */ validateBackupConfig(backupConfig, result) { if (backupConfig.enabled && backupConfig.retention < 1) { result.errors.push({ field: 'backup.retention', message: 'Backup retention must be at least 1 day', code: 'INVALID_RETENTION', value: backupConfig.retention }); result.valid = false; } // Validate backup schedule if provided if (backupConfig.schedule) { if (!this.isValidCronExpression(backupConfig.schedule)) { result.errors.push({ field: 'backup.schedule', message: 'Invalid cron expression format', code: 'INVALID_CRON', value: backupConfig.schedule }); result.valid = false; } } } /** * Validate monitoring configuration */ validateMonitoringConfig(monitoringConfig, result) { if (monitoringConfig.enabled && monitoringConfig.refreshInterval < 1) { result.errors.push({ field: 'monitoring.refreshInterval', message: 'Refresh interval must be at least 1 second', code: 'INVALID_INTERVAL', value: monitoringConfig.refreshInterval }); result.valid = false; } // Validate monitoring URLs for (const [serviceName, url] of Object.entries(monitoringConfig.urls || {})) { if (typeof url === 'string' && !this.isValidUrl(url)) { result.warnings.push({ field: `monitoring.urls.${serviceName}`, message: `Invalid URL format: ${url}`, suggestion: 'Ensure URLs are properly formatted with protocol' }); } } // Validate alert thresholds if (monitoringConfig.alerts?.thresholds) { const thresholds = monitoringConfig.alerts.thresholds; for (const [metric, value] of Object.entries(thresholds)) { if (typeof value === 'number' && (value < 0 || value > 100)) { result.errors.push({ field: `monitoring.alerts.thresholds.${metric}`, message: `Threshold value must be between 0 and 100`, code: 'INVALID_THRESHOLD', value }); result.valid = false; } } } } /** * Check for common misconfigurations */ checkCommonMisconfigurations(config, result) { // Check for port conflicts between services const usedPorts = new Map(); for (const [serviceName, serviceConfig] of Object.entries(config.services)) { if (serviceConfig.port) { if (!usedPorts.has(serviceConfig.port)) { usedPorts.set(serviceConfig.port, []); } usedPorts.get(serviceConfig.port)?.push(serviceName); } } for (const [port, services] of usedPorts) { if (services.length > 1) { result.warnings.push({ field: 'services', message: `Port ${port} is used by multiple services: ${services.join(', ')}`, suggestion: 'Ensure each service uses a unique port' }); } } // Check for missing health checks on critical services const criticalServices = ['database', 'db', 'postgres', 'mysql', 'mongodb', 'redis']; for (const [serviceName, serviceConfig] of Object.entries(config.services)) { if (criticalServices.some(critical => serviceName.toLowerCase().includes(critical))) { if (!serviceConfig.healthCheck) { result.warnings.push({ field: `services.${serviceName}.healthCheck`, message: `Critical service "${serviceName}" should have health checks enabled`, suggestion: 'Enable health checks for better monitoring' }); } } } } /** * Validate service name format */ isValidServiceName(name) { // Docker service names should be lowercase, alphanumeric with hyphens/underscores const serviceNameRegex = /^[a-z0-9][a-z0-9_-]*$/; return serviceNameRegex.test(name) && name.length <= 63; } /** * Validate port number */ isValidPort(port) { return Number.isInteger(port) && port >= 1 && port <= 65535; } /** * Check for common port conflicts */ checkPortConflicts(serviceName, port, result) { const wellKnownPorts = { 22: 'SSH', 23: 'Telnet', 25: 'SMTP', 53: 'DNS', 80: 'HTTP', 110: 'POP3', 143: 'IMAP', 443: 'HTTPS', 993: 'IMAPS', 995: 'POP3S' }; if (wellKnownPorts[port]) { result.warnings.push({ field: `services.${serviceName}.port`, message: `Port ${port} is commonly used by ${wellKnownPorts[port]}`, suggestion: 'Consider using a different port to avoid conflicts' }); } } /** * Validate environment variables */ validateEnvironmentVariables(serviceName, env, result) { for (const [key, value] of Object.entries(env)) { // Check for valid environment variable names if (!/^[A-Z_][A-Z0-9_]*$/i.test(key)) { result.warnings.push({ field: `services.${serviceName}.environment.${key}`, message: 'Environment variable names should contain only letters, numbers, and underscores', suggestion: 'Use standard environment variable naming conventions' }); } // Check for sensitive data in plain text if (this.containsSensitiveData(key, value)) { result.warnings.push({ field: `services.${serviceName}.environment.${key}`, message: 'Sensitive data detected in environment variable', suggestion: 'Consider using Docker secrets or external configuration' }); } } } /** * Validate volume mounts */ validateVolumes(serviceName, volumes, result) { for (const volume of volumes) { if (volume.includes(':')) { const [source] = volume.split(':'); // Check if source path is absolute and exists if (source && (source.startsWith('/') || source.match(/^[A-Z]:\\/))) { // This is a bind mount - source should exist result.warnings.push({ field: `services.${serviceName}.volumes`, message: `Bind mount source should exist: ${source}`, suggestion: 'Ensure the source directory exists before starting the service' }); } } } } /** * Validate CPU limit format */ validateCpuLimit(serviceName, cpuLimit, result) { const cpuRegex = /^(\d+(\.\d+)?)(m|$)/; if (!cpuRegex.test(cpuLimit)) { result.errors.push({ field: `services.${serviceName}.cpu_limit`, message: 'Invalid CPU limit format', code: 'INVALID_CPU_LIMIT', value: cpuLimit }); result.valid = false; } } /** * Validate memory limit format */ validateMemoryLimit(serviceName, memoryLimit, result) { const memoryRegex = /^(\d+)(b|k|m|g|t|ki|mi|gi|ti)$/i; if (!memoryRegex.test(memoryLimit)) { result.errors.push({ field: `services.${serviceName}.memory_limit`, message: 'Invalid memory limit format', code: 'INVALID_MEMORY_LIMIT', value: memoryLimit }); result.valid = false; } } /** * Check if value contains sensitive data */ containsSensitiveData(key, value) { const sensitiveKeywords = [ 'password', 'secret', 'key', 'token', 'auth', 'credential', 'pass', 'pwd', 'private', 'secure' ]; const keyLower = key.toLowerCase(); return sensitiveKeywords.some(keyword => keyLower.includes(keyword)) && value.length > 0 && !value.startsWith('${') && // Not a variable reference !value.startsWith('/run/secrets/'); // Not a Docker secret } /** * Validate URL format */ isValidUrl(url) { try { new URL(url); return true; } catch { return false; } } /** * Validate cron expression (basic validation) */ isValidCronExpression(cron) { const cronRegex = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)(\s+(\S+))?$/; return cronRegex.test(cron); } /** * Validate version string */ validateVersion(version) { return semver.valid(version) !== null; } /** * Validate Docker Compose file structure */ async validateDockerComposeStructure(composeData) { const result = { valid: true, errors: [], warnings: [] }; if (!composeData.services) { result.valid = false; result.errors.push({ field: 'services', message: 'Docker Compose file must contain a services section', code: 'MISSING_SERVICES' }); return result; } // Validate each service for (const [serviceName, serviceConfig] of Object.entries(composeData.services)) { this.validateDockerComposeService(serviceName, serviceConfig, result); } return result; } /** * Validate individual Docker Compose service */ validateDockerComposeService(serviceName, serviceConfig, result) { // Must have either image or build if (!serviceConfig.image && !serviceConfig.build) { result.warnings.push({ field: `services.${serviceName}`, message: 'Service should specify either "image" or "build"', suggestion: 'Add an image reference or build configuration' }); } // Validate port mappings if (serviceConfig.ports) { for (const port of serviceConfig.ports) { if (typeof port === 'string' && !this.isValidPortMapping(port)) { result.warnings.push({ field: `services.${serviceName}.ports`, message: `Invalid port mapping format: ${port}`, suggestion: 'Use format "host_port:container_port" or just "port"' }); } } } // Validate depends_on references if (serviceConfig.depends_on) { // This would need access to all services to validate references // For now, just warn about common issues if (Array.isArray(serviceConfig.depends_on)) { for (const dependency of serviceConfig.depends_on) { if (dependency === serviceName) { result.errors.push({ field: `services.${serviceName}.depends_on`, message: 'Service cannot depend on itself', code: 'CIRCULAR_DEPENDENCY', value: dependency }); result.valid = false; } } } } } /** * Validate Docker port mapping format */ isValidPortMapping(portMapping) { // Valid formats: "8080:80", "8080:80/tcp", "80", "127.0.0.1:8080:80" const portRegex = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:)?(\d+:)?\d+(\/tcp|\/udp)?$/; return portRegex.test(portMapping); } } exports.ValidationUtils = ValidationUtils; //# sourceMappingURL=ValidationUtils.js.map