Upload files to "internal/config"
This commit is contained in:
699
internal/config/validator.go
Normal file
699
internal/config/validator.go
Normal file
@@ -0,0 +1,699 @@
|
||||
/*
|
||||
* 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user