rara/packages/go/config/config.go
toki fdc86c7ff4
Some checks are pending
ci / validate (push) Waiting to run
initial commit
2026-07-18 18:41:17 +09:00

188 lines
5.1 KiB
Go

package config
import (
"errors"
"fmt"
"os"
"strconv"
"time"
"gopkg.in/yaml.v3"
)
type Config struct {
Service Service `yaml:"service"`
Database Database `yaml:"database"`
Artifact Artifact `yaml:"artifact"`
Worker Worker `yaml:"worker"`
Logging Logging `yaml:"logging"`
Observability Observability `yaml:"observability"`
}
type Service struct {
Name string `yaml:"name"`
Listen string `yaml:"listen"`
}
type Database struct {
URL string `yaml:"url"`
MaxConnections int32 `yaml:"max_connections"`
MinConnections int32 `yaml:"min_connections"`
}
type Artifact struct {
Driver string `yaml:"driver"`
Root string `yaml:"root"`
}
type Worker struct {
ID string `yaml:"id"`
ExecutorKind string `yaml:"executor_kind"`
PollInterval string `yaml:"poll_interval"`
LeaseDuration string `yaml:"lease_duration"`
RetryDelay string `yaml:"retry_delay"`
ShutdownGrace string `yaml:"shutdown_grace"`
}
type Logging struct {
Level string `yaml:"level"`
Pretty bool `yaml:"pretty"`
}
type Observability struct {
OTelEndpoint string `yaml:"otel_endpoint"`
OTelInsecure bool `yaml:"otel_insecure"`
}
func Load(path string) (Config, error) {
raw, err := os.ReadFile(path)
if err != nil {
return Config{}, fmt.Errorf("read config: %w", err)
}
var cfg Config
if err := yaml.Unmarshal(raw, &cfg); err != nil {
return Config{}, fmt.Errorf("decode config: %w", err)
}
applyEnvironment(&cfg)
applyDefaults(&cfg)
if err := cfg.Validate(); err != nil {
return Config{}, err
}
return cfg, nil
}
func (c Config) Validate() error {
switch {
case c.Service.Name == "":
return errors.New("service.name is required")
case c.Service.Listen == "":
return errors.New("service.listen is required")
case c.Database.URL == "":
return errors.New("database.url is required")
case c.Database.MaxConnections <= 0:
return errors.New("database.max_connections must be positive")
case c.Database.MinConnections < 0:
return errors.New("database.min_connections cannot be negative")
case c.Database.MinConnections > c.Database.MaxConnections:
return errors.New("database.min_connections cannot exceed max_connections")
case c.Artifact.Driver == "":
return errors.New("artifact.driver is required")
case c.Artifact.Driver == "filesystem" && c.Artifact.Root == "":
return errors.New("artifact.root is required for the filesystem driver")
}
if c.Worker.ID != "" {
if c.Worker.ExecutorKind == "" {
return errors.New("worker.executor_kind is required")
}
if _, err := c.WorkerPollInterval(); err != nil {
return err
}
if _, err := c.WorkerLeaseDuration(); err != nil {
return err
}
}
return nil
}
func (c Config) WorkerPollInterval() (time.Duration, error) {
return parsePositiveDuration("worker.poll_interval", c.Worker.PollInterval)
}
func (c Config) WorkerLeaseDuration() (time.Duration, error) {
return parsePositiveDuration("worker.lease_duration", c.Worker.LeaseDuration)
}
func (c Config) WorkerRetryDelay() (time.Duration, error) {
return parsePositiveDuration("worker.retry_delay", c.Worker.RetryDelay)
}
func (c Config) WorkerShutdownGrace() (time.Duration, error) {
return parsePositiveDuration("worker.shutdown_grace", c.Worker.ShutdownGrace)
}
func parsePositiveDuration(field, value string) (time.Duration, error) {
duration, err := time.ParseDuration(value)
if err != nil {
return 0, fmt.Errorf("%s: %w", field, err)
}
if duration <= 0 {
return 0, fmt.Errorf("%s must be positive", field)
}
return duration, nil
}
func applyDefaults(cfg *Config) {
if cfg.Database.MaxConnections == 0 {
cfg.Database.MaxConnections = 10
}
if cfg.Artifact.Driver == "" {
cfg.Artifact.Driver = "filesystem"
}
if cfg.Logging.Level == "" {
cfg.Logging.Level = "info"
}
if cfg.Worker.PollInterval == "" {
cfg.Worker.PollInterval = "2s"
}
if cfg.Worker.LeaseDuration == "" {
cfg.Worker.LeaseDuration = "30s"
}
if cfg.Worker.RetryDelay == "" {
cfg.Worker.RetryDelay = "5s"
}
if cfg.Worker.ShutdownGrace == "" {
cfg.Worker.ShutdownGrace = "20s"
}
}
func applyEnvironment(cfg *Config) {
overrideString(&cfg.Database.URL, "RARA_DATABASE_URL")
overrideString(&cfg.Artifact.Root, "RARA_ARTIFACT_ROOT")
overrideString(&cfg.Logging.Level, "RARA_LOG_LEVEL")
overrideString(&cfg.Observability.OTelEndpoint, "RARA_OTEL_ENDPOINT")
overrideString(&cfg.Service.Listen, "RARA_LISTEN")
overrideString(&cfg.Worker.ID, "RARA_WORKER_ID")
overrideString(&cfg.Worker.ExecutorKind, "RARA_WORKER_EXECUTOR_KIND")
overrideString(&cfg.Worker.PollInterval, "RARA_WORKER_POLL_INTERVAL")
overrideString(&cfg.Worker.LeaseDuration, "RARA_WORKER_LEASE_DURATION")
overrideInt32(&cfg.Database.MaxConnections, "RARA_DATABASE_MAX_CONNECTIONS")
overrideInt32(&cfg.Database.MinConnections, "RARA_DATABASE_MIN_CONNECTIONS")
}
func overrideString(target *string, key string) {
if value, ok := os.LookupEnv(key); ok {
*target = value
}
}
func overrideInt32(target *int32, key string) {
value, ok := os.LookupEnv(key)
if !ok {
return
}
parsed, err := strconv.ParseInt(value, 10, 32)
if err == nil {
*target = int32(parsed)
}
}