diff --git a/internal/config/validator.go b/internal/config/validator.go deleted file mode 100644 index f608f7e..0000000 --- a/internal/config/validator.go +++ /dev/null @@ -1,699 +0,0 @@ -/* - * Copyright 2026 Safronov Grigorii - * - * Licensed under the CDDL, Version 1.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * https://opensource.org/licenses/CDDL-1.0 - */ - -// Файл: internal/config/validator.go -// Назначение: Полная валидация файла конфигурации config.toml - -package config - -import ( - "fmt" - "net" - "os" - "path/filepath" - "regexp" - "strconv" - "strings" -) - -// ValidationError представляет ошибку валидации -type ValidationError struct { - Field string - Value interface{} - Message string -} - -func (e *ValidationError) Error() string { - return fmt.Sprintf("validation error in %s: %s (value: %v)", e.Field, e.Message, e.Value) -} - -// ValidationResult содержит результат валидации -type ValidationResult struct { - Valid bool - Errors []*ValidationError - Warnings []*ValidationError -} - -// ValidateConfig выполняет полную валидацию конфигурации -func ValidateConfig(cfg *Config) *ValidationResult { - result := &ValidationResult{ - Valid: true, - Errors: make([]*ValidationError, 0), - Warnings: make([]*ValidationError, 0), - } - - // Валидация всех секций - validateClusterConfig(cfg, result) - validateStorageConfig(cfg, result) - validateReplConfig(cfg, result) - validateLogConfig(cfg, result) - validateAPIConfig(cfg, result) - validateReplicationConfig(cfg, result) - validatePluginsConfig(cfg, result) - validateCompressionConfig(cfg, result) - validateWebUIConfig(cfg, result) - validatePerformanceConfig(cfg, result) - validateSecurityConfig(cfg, result) - validateMonitoringConfig(cfg, result) - validateRecoveryConfig(cfg, result) - validateWALConfig(cfg, result) - validateMVCCConfig(cfg, result) - validateTransactionsConfig(cfg, result) - validateACLConfig(cfg, result) - - // Проверка конфликтов портов - validatePortConflicts(cfg, result) - - // Проверка путей - validatePaths(cfg, result) - - result.Valid = len(result.Errors) == 0 - - return result -} - -// validateClusterConfig валидирует секцию cluster -func validateClusterConfig(cfg *Config, result *ValidationResult) { - // Имя кластера - if cfg.Cluster.Name == "" { - result.Errors = append(result.Errors, &ValidationError{ - Field: "cluster.name", - Value: cfg.Cluster.Name, - Message: "cluster name cannot be empty", - }) - } else if len(cfg.Cluster.Name) > 255 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "cluster.name", - Value: cfg.Cluster.Name, - Message: "cluster name too long (max 255 characters)", - }) - } else { - matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, cfg.Cluster.Name) - if !matched { - result.Warnings = append(result.Warnings, &ValidationError{ - Field: "cluster.name", - Value: cfg.Cluster.Name, - Message: "cluster name should contain only letters, numbers, underscores and hyphens", - }) - } - } - - // IP адрес узла - if cfg.Cluster.NodeIP == "" { - result.Errors = append(result.Errors, &ValidationError{ - Field: "cluster.node_ip", - Value: cfg.Cluster.NodeIP, - Message: "node_ip cannot be empty", - }) - } else if cfg.Cluster.NodeIP != "0.0.0.0" && net.ParseIP(cfg.Cluster.NodeIP) == nil { - result.Errors = append(result.Errors, &ValidationError{ - Field: "cluster.node_ip", - Value: cfg.Cluster.NodeIP, - Message: "invalid IP address", - }) - } - - // Порт узла - if cfg.Cluster.NodePort <= 0 || cfg.Cluster.NodePort > 65535 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "cluster.node_port", - Value: cfg.Cluster.NodePort, - Message: "port must be between 1 and 65535", - }) - } - - // Порт Raft - if cfg.Cluster.RaftPort <= 0 || cfg.Cluster.RaftPort > 65535 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "cluster.raft_port", - Value: cfg.Cluster.RaftPort, - Message: "port must be between 1 and 65535", - }) - } - - if cfg.Cluster.NodePort == cfg.Cluster.RaftPort { - result.Errors = append(result.Errors, &ValidationError{ - Field: "cluster", - Value: fmt.Sprintf("node_port=%d, raft_port=%d", cfg.Cluster.NodePort, cfg.Cluster.RaftPort), - Message: "node_port and raft_port cannot be the same", - }) - } - - // Директория данных Raft - if cfg.Cluster.RaftDataDir == "" { - result.Errors = append(result.Errors, &ValidationError{ - Field: "cluster.raft_data_dir", - Value: cfg.Cluster.RaftDataDir, - Message: "raft_data_dir cannot be empty", - }) - } - - // Таймауты - if cfg.Cluster.HeartbeatTimeoutMs < 100 { - result.Warnings = append(result.Warnings, &ValidationError{ - Field: "cluster.heartbeat_timeout_ms", - Value: cfg.Cluster.HeartbeatTimeoutMs, - Message: "heartbeat_timeout_ms is very low (minimum recommended: 100ms)", - }) - } - - if cfg.Cluster.ElectionTimeoutMs < 200 { - result.Warnings = append(result.Warnings, &ValidationError{ - Field: "cluster.election_timeout_ms", - Value: cfg.Cluster.ElectionTimeoutMs, - Message: "election_timeout_ms is very low (minimum recommended: 200ms)", - }) - } - - // Snapshot интервал - if cfg.Cluster.SnapshotIntervalMin < 1 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "cluster.snapshot_interval_min", - Value: cfg.Cluster.SnapshotIntervalMin, - Message: "snapshot_interval_min must be at least 1 minute", - }) - } - - // Snapshot порог - if cfg.Cluster.SnapshotThreshold < 100 { - result.Warnings = append(result.Warnings, &ValidationError{ - Field: "cluster.snapshot_threshold", - Value: cfg.Cluster.SnapshotThreshold, - Message: "snapshot_threshold is very low (minimum recommended: 100)", - }) - } - - // Приоритетная зона - if cfg.Cluster.PriorityZone < 0 || cfg.Cluster.PriorityZone > 9 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "cluster.priority_zone", - Value: cfg.Cluster.PriorityZone, - Message: "priority_zone must be between 0 and 9", - }) - } - - // Список узлов - if !cfg.Cluster.Bootstrap && len(cfg.Cluster.Nodes) == 0 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "cluster.nodes", - Value: cfg.Cluster.Nodes, - Message: "nodes list cannot be empty when bootstrap is false", - }) - } - - for i, node := range cfg.Cluster.Nodes { - parts := strings.Split(node, ":") - if len(parts) != 2 { - result.Errors = append(result.Errors, &ValidationError{ - Field: fmt.Sprintf("cluster.nodes[%d]", i), - Value: node, - Message: "invalid node format, expected ip:port", - }) - continue - } - - if net.ParseIP(parts[0]) == nil && parts[0] != "localhost" { - result.Errors = append(result.Errors, &ValidationError{ - Field: fmt.Sprintf("cluster.nodes[%d].ip", i), - Value: parts[0], - Message: "invalid IP address", - }) - } - - port, err := strconv.Atoi(parts[1]) - if err != nil || port <= 0 || port > 65535 { - result.Errors = append(result.Errors, &ValidationError{ - Field: fmt.Sprintf("cluster.nodes[%d].port", i), - Value: parts[1], - Message: "invalid port number", - }) - } - } -} - -// validateStorageConfig валидирует секцию storage -func validateStorageConfig(cfg *Config, result *ValidationResult) { - if cfg.Storage.PageSizeMB <= 0 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "storage.page_size_mb", - Value: cfg.Storage.PageSizeMB, - Message: "page_size_mb must be positive", - }) - } else if cfg.Storage.PageSizeMB > 1024 { - result.Warnings = append(result.Warnings, &ValidationError{ - Field: "storage.page_size_mb", - Value: cfg.Storage.PageSizeMB, - Message: "page_size_mb is very large (> 1GB), may cause performance issues", - }) - } - - if cfg.Storage.MaxCollections <= 0 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "storage.max_collections", - Value: cfg.Storage.MaxCollections, - Message: "max_collections must be positive", - }) - } - - if cfg.Storage.MaxDocumentsPerCollection <= 0 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "storage.max_documents_per_collection", - Value: cfg.Storage.MaxDocumentsPerCollection, - Message: "max_documents_per_collection must be positive", - }) - } -} - -// validateReplConfig валидирует секцию repl -func validateReplConfig(cfg *Config, result *ValidationResult) { - if cfg.Repl.HistorySize < 0 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "repl.history_size", - Value: cfg.Repl.HistorySize, - Message: "history_size cannot be negative", - }) - } -} - -// validateLogConfig валидирует секцию log -func validateLogConfig(cfg *Config, result *ValidationResult) { - validLevels := map[string]bool{ - "debug": true, "info": true, "warn": true, "error": true, "fatal": true, - } - - if cfg.Log.LogLevel != "" && !validLevels[strings.ToLower(cfg.Log.LogLevel)] { - result.Errors = append(result.Errors, &ValidationError{ - Field: "log.log_level", - Value: cfg.Log.LogLevel, - Message: "invalid log level, expected: debug, info, warn, error, fatal", - }) - } -} - -// validateAPIConfig валидирует секцию api -func validateAPIConfig(cfg *Config, result *ValidationResult) { - if cfg.API.Port <= 0 || cfg.API.Port > 65535 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "api.port", - Value: cfg.API.Port, - Message: "port must be between 1 and 65535", - }) - } -} - -// validateReplicationConfig валидирует секцию replication -func validateReplicationConfig(cfg *Config, result *ValidationResult) { - if cfg.Replication.Enabled && cfg.Replication.ReplicationTimeoutMs < 100 { - result.Warnings = append(result.Warnings, &ValidationError{ - Field: "replication.replication_timeout_ms", - Value: cfg.Replication.ReplicationTimeoutMs, - Message: "replication_timeout_ms is very low (minimum recommended: 100ms)", - }) - } - - if cfg.Replication.MasterMaster && !cfg.Replication.Enabled { - result.Errors = append(result.Errors, &ValidationError{ - Field: "replication.master_master", - Value: cfg.Replication.MasterMaster, - Message: "master_master requires replication.enabled to be true", - }) - } - - if cfg.Replication.SyncReplication && !cfg.Replication.Enabled { - result.Errors = append(result.Errors, &ValidationError{ - Field: "replication.sync_replication", - Value: cfg.Replication.SyncReplication, - Message: "sync_replication requires replication.enabled to be true", - }) - } -} - -// validatePluginsConfig валидирует секцию plugins -func validatePluginsConfig(cfg *Config, result *ValidationResult) { - if cfg.Plugins.Enabled { - if cfg.Plugins.ScriptDir == "" { - result.Errors = append(result.Errors, &ValidationError{ - Field: "plugins.script_dir", - Value: cfg.Plugins.ScriptDir, - Message: "script_dir cannot be empty when plugins are enabled", - }) - } - - if cfg.Plugins.MaxCPUTimeMs < 0 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "plugins.max_cpu_time_ms", - Value: cfg.Plugins.MaxCPUTimeMs, - Message: "max_cpu_time_ms cannot be negative", - }) - } - - if cfg.Plugins.MaxMemoryMB < 0 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "plugins.max_memory_mb", - Value: cfg.Plugins.MaxMemoryMB, - Message: "max_memory_mb cannot be negative", - }) - } - - if cfg.Plugins.MaxExecutionTimeSec < 1 { - result.Warnings = append(result.Warnings, &ValidationError{ - Field: "plugins.max_execution_time_sec", - Value: cfg.Plugins.MaxExecutionTimeSec, - Message: "max_execution_time_sec is very low (minimum 1 second)", - }) - } - } -} - -// validateCompressionConfig валидирует секцию compression -func validateCompressionConfig(cfg *Config, result *ValidationResult) { - if cfg.Compression.Enabled { - validAlgorithms := map[string]bool{"snappy": true, "lz4": true, "zstd": true} - if !validAlgorithms[cfg.Compression.Algorithm] { - result.Errors = append(result.Errors, &ValidationError{ - Field: "compression.algorithm", - Value: cfg.Compression.Algorithm, - Message: "invalid compression algorithm, expected: snappy, lz4, zstd", - }) - } - - if cfg.Compression.Level < 1 || cfg.Compression.Level > 9 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "compression.level", - Value: cfg.Compression.Level, - Message: "compression level must be between 1 and 9", - }) - } - - if cfg.Compression.MinSize < 0 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "compression.min_size", - Value: cfg.Compression.MinSize, - Message: "min_size cannot be negative", - }) - } - } -} - -// validateWebUIConfig валидирует секцию webui -func validateWebUIConfig(cfg *Config, result *ValidationResult) { - if cfg.WebUI.Enabled { - if cfg.WebUI.Port <= 0 || cfg.WebUI.Port > 65535 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "webui.port", - Value: cfg.WebUI.Port, - Message: "port must be between 1 and 65535", - }) - } - - validThemes := map[string]bool{"dark": true, "light": true} - if !validThemes[cfg.WebUI.Theme] { - result.Warnings = append(result.Warnings, &ValidationError{ - Field: "webui.theme", - Value: cfg.WebUI.Theme, - Message: "unknown theme, expected: dark, light", - }) - } - } -} - -// validatePerformanceConfig валидирует секцию performance -func validatePerformanceConfig(cfg *Config, result *ValidationResult) { - if cfg.Performance.BatchSize < 1 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "performance.batch_size", - Value: cfg.Performance.BatchSize, - Message: "batch_size must be at least 1", - }) - } - - if cfg.Performance.MaxConnections < 1 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "performance.max_connections", - Value: cfg.Performance.MaxConnections, - Message: "max_connections must be at least 1", - }) - } - - if cfg.Performance.MaxConnections > 10000 { - result.Warnings = append(result.Warnings, &ValidationError{ - Field: "performance.max_connections", - Value: cfg.Performance.MaxConnections, - Message: "max_connections is very high, may cause resource exhaustion", - }) - } -} - -// validateSecurityConfig валидирует секцию security -func validateSecurityConfig(cfg *Config, result *ValidationResult) { - if cfg.Security.EnableTLS { - if cfg.Security.CertFile == "" { - result.Errors = append(result.Errors, &ValidationError{ - Field: "security.cert_file", - Value: cfg.Security.CertFile, - Message: "cert_file is required when TLS is enabled", - }) - } - - if cfg.Security.KeyFile == "" { - result.Errors = append(result.Errors, &ValidationError{ - Field: "security.key_file", - Value: cfg.Security.KeyFile, - Message: "key_file is required when TLS is enabled", - }) - } - - validVersions := map[string]bool{"1.0": true, "1.1": true, "1.2": true, "1.3": true} - if !validVersions[cfg.Security.MinVersion] { - result.Errors = append(result.Errors, &ValidationError{ - Field: "security.min_version", - Value: cfg.Security.MinVersion, - Message: "invalid TLS version, expected: 1.0, 1.1, 1.2, 1.3", - }) - } - } -} - -// validateMonitoringConfig валидирует секцию monitoring -func validateMonitoringConfig(cfg *Config, result *ValidationResult) { - if cfg.Monitoring.EnableMetrics { - if cfg.Monitoring.MetricsPort <= 0 || cfg.Monitoring.MetricsPort > 65535 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "monitoring.metrics_port", - Value: cfg.Monitoring.MetricsPort, - Message: "port must be between 1 and 65535", - }) - } - } - - if cfg.Monitoring.TraceSampleRate < 0 || cfg.Monitoring.TraceSampleRate > 1 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "monitoring.trace_sample_rate", - Value: cfg.Monitoring.TraceSampleRate, - Message: "trace_sample_rate must be between 0 and 1", - }) - } -} - -// validateRecoveryConfig валидирует секцию recovery -func validateRecoveryConfig(cfg *Config, result *ValidationResult) { - if cfg.Recovery.MaxRetrySec < 1 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "recovery.max_retry_sec", - Value: cfg.Recovery.MaxRetrySec, - Message: "max_retry_sec must be at least 1 second", - }) - } - - if cfg.Recovery.DataReplicationTimeoutSec < 1 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "recovery.data_replication_timeout_sec", - Value: cfg.Recovery.DataReplicationTimeoutSec, - Message: "data_replication_timeout_sec must be at least 1 second", - }) - } -} - -// validateWALConfig валидирует секцию wal -func validateWALConfig(cfg *Config, result *ValidationResult) { - if cfg.WAL.Enabled { - if cfg.WAL.SegmentSizeMB < 1 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "wal.segment_size_mb", - Value: cfg.WAL.SegmentSizeMB, - Message: "segment_size_mb must be at least 1 MB", - }) - } - - if cfg.WAL.SyncIntervalSec < 1 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "wal.sync_interval_sec", - Value: cfg.WAL.SyncIntervalSec, - Message: "sync_interval_sec must be at least 1 second", - }) - } - - if cfg.WAL.RecoveryWorkers < 1 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "wal.recovery_workers", - Value: cfg.WAL.RecoveryWorkers, - Message: "recovery_workers must be at least 1", - }) - } - - if cfg.WAL.RecoveryWorkers > 32 { - result.Warnings = append(result.Warnings, &ValidationError{ - Field: "wal.recovery_workers", - Value: cfg.WAL.RecoveryWorkers, - Message: "recovery_workers is very high (> 32), may cause performance issues", - }) - } - } -} - -// validateMVCCConfig валидирует секцию mvcc -func validateMVCCConfig(cfg *Config, result *ValidationResult) { - if cfg.MVCC.MaxVersionsPerDoc < 0 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "mvcc.max_versions_per_doc", - Value: cfg.MVCC.MaxVersionsPerDoc, - Message: "max_versions_per_doc cannot be negative", - }) - } - - if cfg.MVCC.MaxVersionsPerDoc > 100 { - result.Warnings = append(result.Warnings, &ValidationError{ - Field: "mvcc.max_versions_per_doc", - Value: cfg.MVCC.MaxVersionsPerDoc, - Message: "max_versions_per_doc is very high (> 100), may cause excessive storage usage", - }) - } - - if cfg.MVCC.RetentionDays < 1 { - result.Warnings = append(result.Warnings, &ValidationError{ - Field: "mvcc.retention_days", - Value: cfg.MVCC.RetentionDays, - Message: "retention_days is less than 1 day", - }) - } -} - -// validateTransactionsConfig валидирует секцию transactions -func validateTransactionsConfig(cfg *Config, result *ValidationResult) { - if cfg.Transactions.Enabled { - if cfg.Transactions.DefaultTimeoutSec < 1 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "transactions.default_timeout_sec", - Value: cfg.Transactions.DefaultTimeoutSec, - Message: "default_timeout_sec must be at least 1 second", - }) - } - - if cfg.Transactions.DeadlockCheckIntervalSec < 1 { - result.Errors = append(result.Errors, &ValidationError{ - Field: "transactions.deadlock_check_interval_sec", - Value: cfg.Transactions.DeadlockCheckIntervalSec, - Message: "deadlock_check_interval_sec must be at least 1 second", - }) - } - } -} - -// validateACLConfig валидирует секцию acl -func validateACLConfig(cfg *Config, result *ValidationResult) { - if cfg.ACL.TemporaryGrantTTLHours < 1 { - result.Warnings = append(result.Warnings, &ValidationError{ - Field: "acl.temporary_grant_ttl_hours", - Value: cfg.ACL.TemporaryGrantTTLHours, - Message: "temporary_grant_ttl_hours is very low", - }) - } -} - -// validatePortConflicts проверяет конфликты портов -func validatePortConflicts(cfg *Config, result *ValidationResult) { - ports := make(map[int]string) - - addPort := func(port int, name string) { - if existing, ok := ports[port]; ok { - result.Errors = append(result.Errors, &ValidationError{ - Field: name, - Value: port, - Message: fmt.Sprintf("port %d conflicts with %s", port, existing), - }) - } else { - ports[port] = name - } - } - - addPort(cfg.Cluster.NodePort, "cluster.node_port") - addPort(cfg.Cluster.RaftPort, "cluster.raft_port") - addPort(cfg.API.Port, "api.port") - - if cfg.WebUI.Enabled { - addPort(cfg.WebUI.Port, "webui.port") - } - - if cfg.Monitoring.EnableMetrics { - addPort(cfg.Monitoring.MetricsPort, "monitoring.metrics_port") - } -} - -// validatePaths проверяет существование путей -func validatePaths(cfg *Config, result *ValidationResult) { - // Проверяем директории на возможность создания - checkDir := func(path, fieldName string) { - if path == "" { - return - } - - dir := path - if !strings.HasSuffix(dir, "/") { - dir = filepath.Dir(dir) - } - - if dir == "" || dir == "." || dir == "/" { - return - } - - if err := os.MkdirAll(dir, 0755); err != nil { - result.Warnings = append(result.Warnings, &ValidationError{ - Field: fieldName, - Value: path, - Message: fmt.Sprintf("cannot create directory: %v", err), - }) - } - } - - checkDir(cfg.Cluster.RaftDataDir, "cluster.raft_data_dir") - checkDir(cfg.Log.LogFile, "log.log_file") - checkDir(cfg.Plugins.ScriptDir, "plugins.script_dir") - - if cfg.Security.EnableTLS { - checkFile := func(path, fieldName string) { - if path == "" { - return - } - if _, err := os.Stat(path); os.IsNotExist(err) { - result.Errors = append(result.Errors, &ValidationError{ - Field: fieldName, - Value: path, - Message: "file does not exist", - }) - } - } - - checkFile(cfg.Security.CertFile, "security.cert_file") - checkFile(cfg.Security.KeyFile, "security.key_file") - if cfg.Security.CAFile != "" { - checkFile(cfg.Security.CAFile, "security.ca_file") - } - } -}