From 4f8d1c64d22cea8026b344dc97da6e32ab6d4ae1 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 5 Mar 2026 23:36:44 +0300 Subject: [PATCH] config: initial loading & env overrides Signed-off-by: NotAShelf Change-Id: Iaa7401a20506a084a2a16882e61ea0bc6a6a6964 --- config.example.yaml | 30 ++++++++ internal/config/config.go | 129 +++++++++++++++++++++++++++++++++ internal/config/config_test.go | 63 ++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 config.example.yaml create mode 100644 internal/config/config_test.go diff --git a/config.example.yaml b/config.example.yaml new file mode 100644 index 0000000..7664f40 --- /dev/null +++ b/config.example.yaml @@ -0,0 +1,30 @@ +server: + listen: ":8080" + read_timeout: 30s + write_timeout: 30s + +upstreams: + - url: "https://cache.nixos.org" + priority: 10 + public_key: "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" + + # Try without a public key + - url: "https://nix-community.cachix.org" + priority: 20 + +cache: + db_path: "/var/lib/ncro/routes.db" + max_entries: 100000 + ttl: 1h + latency_alpha: 0.3 + +mesh: + enabled: false + bind_addr: "0.0.0.0:7946" + peers: [] + private_key: "/etc/ncro/node.key" + gossip_interval: 30s + +logging: + level: "info" + format: "json" diff --git a/internal/config/config.go b/internal/config/config.go index d912156..6c98f5a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1 +1,130 @@ package config + +import ( + "fmt" + "os" + "time" + + "gopkg.in/yaml.v3" +) + +// Duration is a wrapper around time.Duration that supports YAML unmarshaling +// from Go duration strings (e.g., "30s", "1h"). yaml.v3 cannot unmarshal +// duration strings directly into time.Duration (int64), so we handle it here. +type Duration struct { + time.Duration +} + +func (d *Duration) UnmarshalYAML(value *yaml.Node) error { + var s string + if err := value.Decode(&s); err != nil { + // Try decoding as a raw int64 (nanoseconds) as fallback. + var ns int64 + if err2 := value.Decode(&ns); err2 != nil { + return fmt.Errorf("cannot unmarshal duration: %w", err) + } + d.Duration = time.Duration(ns) + return nil + } + parsed, err := time.ParseDuration(s) + if err != nil { + return fmt.Errorf("invalid duration %q: %w", s, err) + } + d.Duration = parsed + return nil +} + +type UpstreamConfig struct { + URL string `yaml:"url"` + Priority int `yaml:"priority"` + PublicKey string `yaml:"public_key"` +} + +type ServerConfig struct { + Listen string `yaml:"listen"` + ReadTimeout Duration `yaml:"read_timeout"` + WriteTimeout Duration `yaml:"write_timeout"` +} + +type CacheConfig struct { + DBPath string `yaml:"db_path"` + MaxEntries int `yaml:"max_entries"` + TTL Duration `yaml:"ttl"` + LatencyAlpha float64 `yaml:"latency_alpha"` +} + +type MeshConfig struct { + Enabled bool `yaml:"enabled"` + BindAddr string `yaml:"bind_addr"` + Peers []string `yaml:"peers"` + PrivateKeyPath string `yaml:"private_key"` + GossipInterval Duration `yaml:"gossip_interval"` +} + +type LoggingConfig struct { + Level string `yaml:"level"` + Format string `yaml:"format"` +} + +type Config struct { + Server ServerConfig `yaml:"server"` + Upstreams []UpstreamConfig `yaml:"upstreams"` + Cache CacheConfig `yaml:"cache"` + Mesh MeshConfig `yaml:"mesh"` + Logging LoggingConfig `yaml:"logging"` +} + +func defaults() Config { + return Config{ + Server: ServerConfig{ + Listen: ":8080", + ReadTimeout: Duration{30 * time.Second}, + WriteTimeout: Duration{30 * time.Second}, + }, + Upstreams: []UpstreamConfig{ + {URL: "https://cache.nixos.org", Priority: 10}, + }, + Cache: CacheConfig{ + DBPath: "/var/lib/ncro/routes.db", + MaxEntries: 100000, + TTL: Duration{time.Hour}, + LatencyAlpha: 0.3, + }, + Mesh: MeshConfig{ + BindAddr: "0.0.0.0:7946", + GossipInterval: Duration{30 * time.Second}, + }, + Logging: LoggingConfig{ + Level: "info", + Format: "json", + }, + } +} + +// Load loads config from file (if non-empty) and applies env overrides. +func Load(path string) (*Config, error) { + cfg := defaults() + + if path != "" { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, err + } + } + + // Env overrides + if v := os.Getenv("NCRO_LISTEN"); v != "" { + cfg.Server.Listen = v + } + if v := os.Getenv("NCRO_DB_PATH"); v != "" { + cfg.Cache.DBPath = v + } + if v := os.Getenv("NCRO_LOG_LEVEL"); v != "" { + cfg.Logging.Level = v + } + + return &cfg, nil +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..7bf0a41 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,63 @@ +package config_test + +import ( + "os" + "testing" + + "notashelf.dev/ncro/internal/config" +) + +func TestLoadDefaults(t *testing.T) { + cfg, err := config.Load("") + if err != nil { + t.Fatalf("Load(\"\") error: %v", err) + } + if cfg.Server.Listen != ":8080" { + t.Errorf("default listen = %q, want :8080", cfg.Server.Listen) + } + if len(cfg.Upstreams) == 0 { + t.Error("expected at least one default upstream") + } + if cfg.Cache.MaxEntries != 100000 { + t.Errorf("default max_entries = %d, want 100000", cfg.Cache.MaxEntries) + } +} + +func TestLoadFromYAML(t *testing.T) { + yamlContent := ` +server: + listen: ":9090" +upstreams: + - url: "https://cache.nixos.org" + priority: 10 +cache: + db_path: "/tmp/test.db" + max_entries: 500 +` + f, _ := os.CreateTemp("", "ncro-*.yaml") + defer os.Remove(f.Name()) + f.WriteString(yamlContent) + f.Close() + + cfg, err := config.Load(f.Name()) + if err != nil { + t.Fatalf("Load error: %v", err) + } + if cfg.Server.Listen != ":9090" { + t.Errorf("listen = %q, want :9090", cfg.Server.Listen) + } + if cfg.Cache.MaxEntries != 500 { + t.Errorf("max_entries = %d, want 500", cfg.Cache.MaxEntries) + } +} + +func TestEnvOverride(t *testing.T) { + t.Setenv("NCRO_LISTEN", ":1234") + cfg, err := config.Load("") + if err != nil { + t.Fatalf("Load error: %v", err) + } + if cfg.Server.Listen != ":1234" { + t.Errorf("env override listen = %q, want :1234", cfg.Server.Listen) + } +}