Upload files to "internal/config"

This commit is contained in:
2026-06-13 19:30:22 +00:00
parent 8b52a6b957
commit ed18b5ad2c

View File

@@ -0,0 +1,692 @@
/*
* 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 представляет ошибку валидации (использует тип error)
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)
}
// ValidateConfigFull выполняет полную валидацию конфигурации (переименовано во избежание конфликта)
func ValidateConfigFull(cfg *Config) *ValidationResult {
result := &ValidationResult{
Valid: true,
Errors: make([]error, 0),
Warnings: make([]error, 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")
}
}
}