187 lines
4.7 KiB
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", 120*time.Second)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
writeTimeout, err := envDuration("WRITE_TIMEOUT", 300*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
|
|
}
|