config: initial loading & env overrides
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Iaa7401a20506a084a2a16882e61ea0bc6a6a6964
This commit is contained in:
parent
356aa999af
commit
4f8d1c64d2
3 changed files with 222 additions and 0 deletions
30
config.example.yaml
Normal file
30
config.example.yaml
Normal file
|
|
@ -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"
|
||||||
|
|
@ -1 +1,130 @@
|
||||||
package config
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
63
internal/config/config_test.go
Normal file
63
internal/config/config_test.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue