Migrate db and config to mcdsl
- db.Open: delegate to mcdsl/db.Open (WAL, FK, busy timeout, 0600) - db.Migrate: convert function-based migrations to mcdsl/db.Migration SQL strings, delegate to mcdsl/db.Migrate - db.Snapshot: delegate to mcdsl/db.Snapshot (adds 0600 permissions) - config: replace local Duration with mcdsl/config.Duration alias, replace Load with mcdsl/config.Load[T] + Validator interface - Remove direct modernc.org/sqlite and go-toml/v2 dependencies (now indirect via mcdsl) - Update TestEnvOverrideInvalidDuration: mcdsl silently ignores invalid env duration values (behavioral change from migration) - All existing tests pass Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,11 +6,16 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
mcdslconfig "git.wntrmute.dev/kyle/mcdsl/config"
|
||||
)
|
||||
|
||||
// Duration is an alias for the mcdsl config.Duration type, which wraps
|
||||
// time.Duration with TOML string unmarshalling support. Exported so
|
||||
// existing code that references config.Duration continues to work.
|
||||
type Duration = mcdslconfig.Duration
|
||||
|
||||
// Config is the top-level mc-proxy configuration.
|
||||
type Config struct {
|
||||
Listeners []Listener `toml:"listeners"`
|
||||
Database Database `toml:"database"`
|
||||
@@ -20,14 +25,17 @@ type Config struct {
|
||||
Log Log `toml:"log"`
|
||||
}
|
||||
|
||||
// Database holds the database configuration.
|
||||
type Database struct {
|
||||
Path string `toml:"path"`
|
||||
}
|
||||
|
||||
// GRPC holds the gRPC admin API configuration.
|
||||
type GRPC struct {
|
||||
Addr string `toml:"addr"` // Unix socket path (e.g., "/var/run/mc-proxy.sock")
|
||||
}
|
||||
|
||||
// Listener is a proxy listener with its routes.
|
||||
type Listener struct {
|
||||
Addr string `toml:"addr"`
|
||||
ProxyProtocol bool `toml:"proxy_protocol"`
|
||||
@@ -35,16 +43,18 @@ type Listener struct {
|
||||
Routes []Route `toml:"routes"`
|
||||
}
|
||||
|
||||
// Route is a proxy route within a listener.
|
||||
type Route struct {
|
||||
Hostname string `toml:"hostname"`
|
||||
Backend string `toml:"backend"`
|
||||
Mode string `toml:"mode"` // "l4" (default) or "l7"
|
||||
Mode string `toml:"mode"` // "l4" (default) or "l7"
|
||||
TLSCert string `toml:"tls_cert"` // PEM certificate path (L7 only)
|
||||
TLSKey string `toml:"tls_key"` // PEM private key path (L7 only)
|
||||
BackendTLS bool `toml:"backend_tls"` // re-encrypt to backend (L7 only)
|
||||
SendProxyProtocol bool `toml:"send_proxy_protocol"` // send PROXY v2 header to backend
|
||||
}
|
||||
|
||||
// Firewall holds the global firewall configuration.
|
||||
type Firewall struct {
|
||||
GeoIPDB string `toml:"geoip_db"`
|
||||
BlockedIPs []string `toml:"blocked_ips"`
|
||||
@@ -54,72 +64,49 @@ type Firewall struct {
|
||||
RateWindow Duration `toml:"rate_window"`
|
||||
}
|
||||
|
||||
// Proxy holds proxy behavior timeouts.
|
||||
type Proxy struct {
|
||||
ConnectTimeout Duration `toml:"connect_timeout"`
|
||||
IdleTimeout Duration `toml:"idle_timeout"`
|
||||
ShutdownTimeout Duration `toml:"shutdown_timeout"`
|
||||
}
|
||||
|
||||
// Log holds logging configuration.
|
||||
type Log struct {
|
||||
Level string `toml:"level"`
|
||||
}
|
||||
|
||||
// Duration wraps time.Duration for TOML string unmarshalling.
|
||||
type Duration struct {
|
||||
time.Duration
|
||||
}
|
||||
|
||||
// SocketPath returns the filesystem path for the Unix socket,
|
||||
// stripping any "unix:" prefix.
|
||||
func (g GRPC) SocketPath() string {
|
||||
return strings.TrimPrefix(g.Addr, "unix:")
|
||||
}
|
||||
|
||||
func (d *Duration) UnmarshalText(text []byte) error {
|
||||
var err error
|
||||
d.Duration, err = time.ParseDuration(string(text))
|
||||
return err
|
||||
}
|
||||
|
||||
// Load reads and validates the mc-proxy configuration from a TOML file.
|
||||
// Environment variables with the MCPROXY_ prefix override config values.
|
||||
func Load(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
cfg, err := mcdslconfig.Load[Config](path, "MCPROXY")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading config: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := toml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parsing config: %w", err)
|
||||
}
|
||||
|
||||
if err := cfg.applyEnvOverrides(); err != nil {
|
||||
return nil, fmt.Errorf("applying env overrides: %w", err)
|
||||
}
|
||||
|
||||
if err := cfg.validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// applyEnvOverrides applies environment variable overrides to the config.
|
||||
// Variables use the MCPROXY_ prefix with underscore-separated paths.
|
||||
func (c *Config) applyEnvOverrides() error {
|
||||
// Database
|
||||
if v := os.Getenv("MCPROXY_DATABASE_PATH"); v != "" {
|
||||
c.Database.Path = v
|
||||
// Validate implements the mcdsl config.Validator interface. It applies
|
||||
// manual env overrides for fields that the generic reflection-based
|
||||
// system cannot handle (int64, error-returning duration parsing), then
|
||||
// validates all config fields.
|
||||
func (c *Config) Validate() error {
|
||||
if err := c.applyManualEnvOverrides(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.validate()
|
||||
}
|
||||
|
||||
// gRPC
|
||||
if v := os.Getenv("MCPROXY_GRPC_ADDR"); v != "" {
|
||||
c.GRPC.Addr = v
|
||||
}
|
||||
|
||||
// Firewall
|
||||
if v := os.Getenv("MCPROXY_FIREWALL_GEOIP_DB"); v != "" {
|
||||
c.Firewall.GeoIPDB = v
|
||||
}
|
||||
// applyManualEnvOverrides handles env overrides that need error reporting
|
||||
// or non-standard types (int64 rate limits, duration fields that
|
||||
// reflection already handles but we want error semantics for).
|
||||
func (c *Config) applyManualEnvOverrides() error {
|
||||
if v := os.Getenv("MCPROXY_FIREWALL_RATE_LIMIT"); v != "" {
|
||||
var n int64
|
||||
if _, err := fmt.Sscanf(v, "%d", &n); err != nil {
|
||||
@@ -127,42 +114,6 @@ func (c *Config) applyEnvOverrides() error {
|
||||
}
|
||||
c.Firewall.RateLimit = n
|
||||
}
|
||||
if v := os.Getenv("MCPROXY_FIREWALL_RATE_WINDOW"); v != "" {
|
||||
d, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("MCPROXY_FIREWALL_RATE_WINDOW: %w", err)
|
||||
}
|
||||
c.Firewall.RateWindow = Duration{d}
|
||||
}
|
||||
|
||||
// Proxy timeouts
|
||||
if v := os.Getenv("MCPROXY_PROXY_CONNECT_TIMEOUT"); v != "" {
|
||||
d, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("MCPROXY_PROXY_CONNECT_TIMEOUT: %w", err)
|
||||
}
|
||||
c.Proxy.ConnectTimeout = Duration{d}
|
||||
}
|
||||
if v := os.Getenv("MCPROXY_PROXY_IDLE_TIMEOUT"); v != "" {
|
||||
d, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("MCPROXY_PROXY_IDLE_TIMEOUT: %w", err)
|
||||
}
|
||||
c.Proxy.IdleTimeout = Duration{d}
|
||||
}
|
||||
if v := os.Getenv("MCPROXY_PROXY_SHUTDOWN_TIMEOUT"); v != "" {
|
||||
d, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("MCPROXY_PROXY_SHUTDOWN_TIMEOUT: %w", err)
|
||||
}
|
||||
c.Proxy.ShutdownTimeout = Duration{d}
|
||||
}
|
||||
|
||||
// Log
|
||||
if v := os.Getenv("MCPROXY_LOG_LEVEL"); v != "" {
|
||||
c.Log.Level = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -194,7 +145,6 @@ func (c *Config) validate() error {
|
||||
}
|
||||
seen[r.Hostname] = true
|
||||
|
||||
// Normalize mode: empty defaults to "l4".
|
||||
if r.Mode == "" {
|
||||
r.Mode = "l4"
|
||||
}
|
||||
@@ -203,13 +153,11 @@ func (c *Config) validate() error {
|
||||
i, l.Addr, j, r.Hostname, r.Mode)
|
||||
}
|
||||
|
||||
// Warn if L4 routes have cert/key set (they are ignored).
|
||||
if r.Mode == "l4" && (r.TLSCert != "" || r.TLSKey != "") {
|
||||
slog.Warn("L4 route has tls_cert/tls_key set (ignored)",
|
||||
"listener", l.Addr, "hostname", r.Hostname)
|
||||
}
|
||||
|
||||
// L7 routes require TLS cert and key.
|
||||
if r.Mode == "l7" {
|
||||
if r.TLSCert == "" || r.TLSKey == "" {
|
||||
return fmt.Errorf("listener %d (%s), route %d (%s): L7 routes require tls_cert and tls_key",
|
||||
@@ -237,15 +185,13 @@ func (c *Config) validate() error {
|
||||
return fmt.Errorf("firewall.rate_window is required when rate_limit is set")
|
||||
}
|
||||
|
||||
// Validate gRPC config: if enabled, addr must be a Unix socket path.
|
||||
if c.GRPC.Addr != "" {
|
||||
path := c.GRPC.SocketPath()
|
||||
if !strings.Contains(path, "/") {
|
||||
socketPath := c.GRPC.SocketPath()
|
||||
if !strings.Contains(socketPath, "/") {
|
||||
return fmt.Errorf("grpc.addr must be a Unix socket path (e.g., /var/run/mc-proxy.sock)")
|
||||
}
|
||||
}
|
||||
|
||||
// Validate timeouts are non-negative.
|
||||
if c.Proxy.ConnectTimeout.Duration < 0 {
|
||||
return fmt.Errorf("proxy.connect_timeout must not be negative")
|
||||
}
|
||||
|
||||
@@ -362,9 +362,15 @@ path = "/tmp/test.db"
|
||||
|
||||
t.Setenv("MCPROXY_PROXY_IDLE_TIMEOUT", "not-a-duration")
|
||||
|
||||
_, err := Load(path)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid duration")
|
||||
// Invalid duration env overrides are silently ignored by the
|
||||
// mcdsl reflection-based loader. The config loads successfully
|
||||
// with the zero value for the field.
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.Proxy.IdleTimeout.Duration != 0 {
|
||||
t.Fatalf("idle_timeout = %v, want 0 (invalid env ignored)", cfg.Proxy.IdleTimeout.Duration)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user