- DatabaseConfig: Host/Port/User/Password/Name/DBName/SSLMode → 单个 DSN 字段 - RedisConfig: Host/Port/Password/DB/URL → 单个 DSN 字段 - 环境变量: GOCHAT_DATABASE_* (7个) → GOCHAT_DATABASE_DSN, GOCHAT_REDIS_* (5个) → GOCHAT_REDIS_DSN - validator.go: DSN URL 解析校验 (scheme + host) - redis.go: redis.ParseURL(cfg.DSN) 直连 - 所有 docker-compose / CI / shell 脚本 / .env 同步更新 - 删除 deploy/helm/ 整个目录 (20个文件) - CI 删除 helm-validate / deploy-staging / deploy-production 三个 job - 文档同步更新 (README, 架构设计, PRD, 滚动升级)
122 lines
3.9 KiB
Go
122 lines
3.9 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Validate checks the configuration for required fields and valid values.
|
|
// Similar to Chatwoot's config validation in config/initializers.
|
|
func Validate(cfg *Config) error {
|
|
// Server validation
|
|
if cfg.Server.Port < 1 || cfg.Server.Port > 65535 {
|
|
return fmt.Errorf("invalid server port: %d", cfg.Server.Port)
|
|
}
|
|
if cfg.Server.Mode != "debug" && cfg.Server.Mode != "release" && cfg.Server.Mode != "test" {
|
|
return fmt.Errorf("invalid server mode: %s (must be debug, release, or test)", cfg.Server.Mode)
|
|
}
|
|
|
|
// Database validation (ref: Chatwoot requires POSTGRES_* env vars)
|
|
if cfg.Database.DSN == "" {
|
|
return fmt.Errorf("database DSN is required")
|
|
}
|
|
dbURL, err := url.Parse(cfg.Database.DSN)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid database DSN: %w", err)
|
|
}
|
|
if dbURL.Scheme != "postgres" && dbURL.Scheme != "postgresql" {
|
|
return fmt.Errorf("invalid database DSN: scheme must be postgres or postgresql, got %s", dbURL.Scheme)
|
|
}
|
|
if dbURL.Host == "" {
|
|
return fmt.Errorf("invalid database DSN: host is required")
|
|
}
|
|
|
|
// Redis validation (ref: Chatwoot config/cable.yml requires Redis connection)
|
|
if cfg.Redis.DSN == "" {
|
|
return fmt.Errorf("redis DSN is required")
|
|
}
|
|
redisURL, err := url.Parse(cfg.Redis.DSN)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid redis DSN: %w", err)
|
|
}
|
|
if redisURL.Scheme != "redis" && redisURL.Scheme != "rediss" {
|
|
return fmt.Errorf("invalid redis DSN: scheme must be redis or rediss, got %s", redisURL.Scheme)
|
|
}
|
|
if redisURL.Host == "" {
|
|
return fmt.Errorf("invalid redis DSN: host is required")
|
|
}
|
|
|
|
// JWT validation
|
|
if cfg.JWT.Secret == "" || cfg.JWT.Secret == "change-me-in-production" {
|
|
if cfg.Server.Mode == "release" {
|
|
return fmt.Errorf("JWT secret must be changed in production mode")
|
|
}
|
|
}
|
|
|
|
// Log validation
|
|
validLogLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true}
|
|
if !validLogLevels[cfg.Log.Level] {
|
|
return fmt.Errorf("invalid log level: %s", cfg.Log.Level)
|
|
}
|
|
|
|
// Worker validation (ref: Chatwoot sidekiq.yml concurrency check)
|
|
if cfg.Worker.Concurrency < 1 {
|
|
return fmt.Errorf("worker concurrency must be >= 1")
|
|
}
|
|
if cfg.Worker.BlockTimeoutS < 1 {
|
|
return fmt.Errorf("worker.redis_block_timeout_s must be >= 1")
|
|
}
|
|
if cfg.Worker.SweepIntervalS < 1 {
|
|
return fmt.Errorf("worker.redis_sweep_interval_s must be >= 1")
|
|
}
|
|
|
|
// Search validation. Meilisearch is the production parity engine; db remains
|
|
// available only as an explicit development fallback.
|
|
engine := strings.ToLower(cfg.Search.Engine)
|
|
if engine == "" {
|
|
engine = "meilisearch"
|
|
}
|
|
if engine != "meilisearch" && engine != "db" {
|
|
return fmt.Errorf("invalid search engine: %s (must be meilisearch or db)", cfg.Search.Engine)
|
|
}
|
|
if engine == "db" && cfg.Server.Mode == "release" {
|
|
return fmt.Errorf("search.engine=db is only allowed for local development; release mode requires meilisearch")
|
|
}
|
|
if engine == "meilisearch" {
|
|
if cfg.Search.Host == "" {
|
|
return fmt.Errorf("search.host is required when search.engine=meilisearch")
|
|
}
|
|
searchURL, err := url.Parse(cfg.Search.Host)
|
|
if err != nil || searchURL.Scheme == "" || searchURL.Host == "" {
|
|
return fmt.Errorf("invalid search.host: %s", cfg.Search.Host)
|
|
}
|
|
}
|
|
if cfg.Search.TimeoutSeconds < 0 {
|
|
return fmt.Errorf("search.timeout_seconds must be >= 0")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ParseStatementTimeout converts the string timeout to time.Duration.
|
|
func ParseStatementTimeout(timeout string) (time.Duration, error) {
|
|
if timeout == "" {
|
|
return 14 * time.Second, nil
|
|
}
|
|
|
|
// Handle plain seconds (e.g., "14s")
|
|
if secs, err := strconv.Atoi(timeout); err == nil {
|
|
return time.Duration(secs) * time.Second, nil
|
|
}
|
|
|
|
// Handle Go duration format (e.g., "14s", "500ms")
|
|
d, err := time.ParseDuration(timeout)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid statement_timeout format: %s", timeout)
|
|
}
|
|
return d, nil
|
|
}
|