Files
core/internal/config/config.go

187 lines
4.7 KiB
Go

package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"time"
)
// Config holds process configuration loaded from environment variables.
type Config struct {
HTTPAddr string
ReadTimeout time.Duration
WriteTimeout time.Duration
ShutdownGrace time.Duration
DatabaseDSN string
MigrationsDir string
}
func Load() (Config, error) {
readTimeout, err := envDuration("READ_TIMEOUT", 10*time.Second)
if err != nil {
return Config{}, err
}
writeTimeout, err := envDuration("WRITE_TIMEOUT", 15*time.Second)
if err != nil {
return Config{}, err
}
shutdownGrace, err := envDuration("SHUTDOWN_GRACE", 10*time.Second)
if err != nil {
return Config{}, err
}
cfg := Config{
HTTPAddr: envOrDefault("HTTP_ADDR", ":9999"),
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
ShutdownGrace: shutdownGrace,
DatabaseDSN: os.Getenv("DATABASE_DSN"),
MigrationsDir: envOrDefault("MIGRATIONS_DIR", "migrations"),
}
fileCfg, err := loadFileConfig()
if err != nil {
return Config{}, err
}
applyFileConfig(&cfg, fileCfg)
return cfg, nil
}
func envOrDefault(key, fallback string) string {
if value := os.Getenv(key); value != "" {
return value
}
return fallback
}
func envDuration(key string, fallback time.Duration) (time.Duration, error) {
value := os.Getenv(key)
if value == "" {
return fallback, nil
}
seconds, err := strconv.Atoi(value)
if err != nil {
return 0, fmt.Errorf("%s must be an integer number of seconds: %w", key, err)
}
if seconds <= 0 {
return 0, fmt.Errorf("%s must be > 0", key)
}
return time.Duration(seconds) * time.Second, nil
}
type fileConfig struct {
HTTPAddr *string `json:"http_addr"`
ReadTimeoutSeconds *int `json:"read_timeout_seconds"`
WriteTimeoutSeconds *int `json:"write_timeout_seconds"`
ShutdownGraceSeconds *int `json:"shutdown_grace_seconds"`
DatabaseDSN *string `json:"database_dsn"`
Database *dbFileConfig `json:"database"`
MigrationsDir *string `json:"migrations_dir"`
}
type dbFileConfig struct {
User *string `json:"user"`
Password *string `json:"password"`
Host *string `json:"host"`
Port *int `json:"port"`
Name *string `json:"name"`
Params *string `json:"params"`
}
func loadFileConfig() (fileConfig, error) {
path := os.Getenv("CONFIG_FILE")
if path == "" {
path = "config.json"
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return fileConfig{}, nil
}
return fileConfig{}, err
}
} else {
if _, err := os.Stat(path); err != nil {
return fileConfig{}, fmt.Errorf("config file %s: %w", path, err)
}
}
raw, err := os.ReadFile(filepath.Clean(path))
if err != nil {
return fileConfig{}, err
}
var cfg fileConfig
if err := json.Unmarshal(raw, &cfg); err != nil {
return fileConfig{}, fmt.Errorf("parse config file %s: %w", path, err)
}
return cfg, nil
}
func applyFileConfig(cfg *Config, fileCfg fileConfig) {
if fileCfg.HTTPAddr != nil && os.Getenv("HTTP_ADDR") == "" {
cfg.HTTPAddr = *fileCfg.HTTPAddr
}
if fileCfg.ReadTimeoutSeconds != nil && os.Getenv("READ_TIMEOUT") == "" {
if *fileCfg.ReadTimeoutSeconds > 0 {
cfg.ReadTimeout = time.Duration(*fileCfg.ReadTimeoutSeconds) * time.Second
}
}
if fileCfg.WriteTimeoutSeconds != nil && os.Getenv("WRITE_TIMEOUT") == "" {
if *fileCfg.WriteTimeoutSeconds > 0 {
cfg.WriteTimeout = time.Duration(*fileCfg.WriteTimeoutSeconds) * time.Second
}
}
if fileCfg.ShutdownGraceSeconds != nil && os.Getenv("SHUTDOWN_GRACE") == "" {
if *fileCfg.ShutdownGraceSeconds > 0 {
cfg.ShutdownGrace = time.Duration(*fileCfg.ShutdownGraceSeconds) * time.Second
}
}
if fileCfg.DatabaseDSN != nil && os.Getenv("DATABASE_DSN") == "" {
cfg.DatabaseDSN = *fileCfg.DatabaseDSN
}
if cfg.DatabaseDSN == "" && os.Getenv("DATABASE_DSN") == "" {
if dsn := buildDSN(fileCfg.Database); dsn != "" {
cfg.DatabaseDSN = dsn
}
}
if fileCfg.MigrationsDir != nil && os.Getenv("MIGRATIONS_DIR") == "" {
cfg.MigrationsDir = *fileCfg.MigrationsDir
}
}
func buildDSN(db *dbFileConfig) string {
if db == nil || db.Name == nil {
return ""
}
user := valueOrDefault(db.User, "reanimator")
password := valueOrDefault(db.Password, "reanimator")
host := valueOrDefault(db.Host, "127.0.0.1")
port := intValueOrDefault(db.Port, 3306)
params := valueOrDefault(db.Params, "parseTime=true")
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?%s", user, password, host, port, *db.Name, params)
}
func valueOrDefault(value *string, fallback string) string {
if value == nil || *value == "" {
return fallback
}
return *value
}
func intValueOrDefault(value *int, fallback int) int {
if value == nil || *value <= 0 {
return fallback
}
return *value
}