188 lines
5.1 KiB
Go
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)
|
|
}
|
|
}
|