-
Notifications
You must be signed in to change notification settings - Fork 0
feat(daemon): add configuration loader and wire into startup #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,142 @@ | ||||||||||||
| package main | ||||||||||||
|
|
||||||||||||
| import ( | ||||||||||||
| "flag" | ||||||||||||
| "fmt" | ||||||||||||
| "net" | ||||||||||||
| "os" | ||||||||||||
| "path/filepath" | ||||||||||||
| "strings" | ||||||||||||
| "time" | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| const ( | ||||||||||||
| defaultPollInterval = 10 * time.Second | ||||||||||||
| defaultListenAddr = "127.0.0.1:8090" | ||||||||||||
| defaultWebMode = "embedded" | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| type Config struct { | ||||||||||||
| DBPath string | ||||||||||||
| PolicyPath string | ||||||||||||
| ListenAddr string | ||||||||||||
| PollInterval time.Duration | ||||||||||||
| WebMode string | ||||||||||||
| WebDir string | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func LoadConfig() (Config, error) { | ||||||||||||
| cwd, err := os.Getwd() | ||||||||||||
| if err != nil { | ||||||||||||
| return Config{}, fmt.Errorf("failed to get cwd: %w", err) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| defaultDBPath := filepath.Join(cwd, "ratelord.db") | ||||||||||||
| defaultPolicyPath := filepath.Join(cwd, "policy.json") | ||||||||||||
|
|
||||||||||||
| dbDefault := envOrDefault("RATELORD_DB_PATH", defaultDBPath) | ||||||||||||
| policyDefault := envOrDefault("RATELORD_POLICY_PATH", defaultPolicyPath) | ||||||||||||
| listenDefault := defaultListenAddr | ||||||||||||
| if portOverride := envOrDefault("RATELORD_PORT", ""); portOverride != "" { | ||||||||||||
| listenDefault = net.JoinHostPort("127.0.0.1", portOverride) | ||||||||||||
| } | ||||||||||||
| listenDefault = envOrDefault("RATELORD_LISTEN_ADDR", listenDefault) | ||||||||||||
| webModeDefault := envOrDefault("RATELORD_WEB_MODE", defaultWebMode) | ||||||||||||
| webDirDefault := envOrDefault("RATELORD_WEB_DIR", "") | ||||||||||||
|
|
||||||||||||
| pollDefault, err := envDuration("RATELORD_POLL_INTERVAL", defaultPollInterval) | ||||||||||||
| if err != nil { | ||||||||||||
| return Config{}, err | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| dbPath := flag.String("db-path", dbDefault, "Path to the SQLite database") | ||||||||||||
| policyPath := flag.String("policy-path", policyDefault, "Path to the policy JSON file") | ||||||||||||
| listenAddr := flag.String("listen-addr", listenDefault, "HTTP listen address") | ||||||||||||
| pollInterval := flag.Duration("poll-interval", pollDefault, "Provider poll interval (e.g. 10s)") | ||||||||||||
| webMode := flag.String("web-mode", webModeDefault, "Web assets mode: embedded, dir, off") | ||||||||||||
| webDir := flag.String("web-dir", webDirDefault, "Web assets directory when web-mode=dir") | ||||||||||||
|
|
||||||||||||
| flag.Parse() | ||||||||||||
|
|
||||||||||||
| cfg := Config{ | ||||||||||||
| DBPath: strings.TrimSpace(*dbPath), | ||||||||||||
| PolicyPath: strings.TrimSpace(*policyPath), | ||||||||||||
| ListenAddr: strings.TrimSpace(*listenAddr), | ||||||||||||
| PollInterval: *pollInterval, | ||||||||||||
| WebMode: strings.TrimSpace(*webMode), | ||||||||||||
| WebDir: strings.TrimSpace(*webDir), | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| return normalizeConfig(cfg) | ||||||||||||
| } | ||||||||||||
|
Comment on lines
+28
to
+71
|
||||||||||||
|
|
||||||||||||
| func normalizeConfig(cfg Config) (Config, error) { | ||||||||||||
| var err error | ||||||||||||
| if cfg.DBPath == "" { | ||||||||||||
| return Config{}, fmt.Errorf("db path is required") | ||||||||||||
| } | ||||||||||||
| if cfg.PolicyPath == "" { | ||||||||||||
| return Config{}, fmt.Errorf("policy path is required") | ||||||||||||
| } | ||||||||||||
| if cfg.ListenAddr == "" { | ||||||||||||
| return Config{}, fmt.Errorf("listen address is required") | ||||||||||||
| } | ||||||||||||
|
||||||||||||
| } | |
| } | |
| if _, _, err := net.SplitHostPort(cfg.ListenAddr); err != nil { | |
| return Config{}, fmt.Errorf("invalid listen address %q: %w", cfg.ListenAddr, err) | |
| } |
Copilot
AI
Feb 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When web_mode is "dir", the config validates that the directory exists and is indeed a directory (lines 112-116). However, this validation happens at startup before the daemon needs to serve assets. If the directory is deleted or becomes inaccessible after the daemon starts, the static file handler will fail at runtime with potentially unclear errors. Consider whether it's preferable to validate directory existence at startup (current behavior) or defer validation until assets are actually served (more resilient to temporary filesystem issues).
| if info, err := os.Stat(cfg.WebDir); err != nil { | |
| return Config{}, fmt.Errorf("stat web dir: %w", err) | |
| } else if !info.IsDir() { | |
| return Config{}, fmt.Errorf("web dir is not a directory: %s", cfg.WebDir) | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling flag.Parse() in LoadConfig() means flags can only be parsed once per process lifetime. If LoadConfig is called multiple times (e.g., in tests or future hot-reload scenarios), subsequent calls will fail or behave unexpectedly. Consider checking if flags have already been parsed using flag.Parsed(), or restructuring to separate flag definition from parsing.