/* * 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") } } }