check / check (push) Canceled after 0s
Takes .golangci.yml, .dockerignore, .gitignore, .editorconfig, the workflow and REPO_POLICIES.md from sneak/prompts 0653579, with this repository's own entries after the shared content. Lint (golangci-lint v2.14.0) and test are Dockerfile phases that script/lint and script/test build alone with a new CACHEBUST value, keeping Go's and golangci-lint's caches; the image build depends on both. make fmt and make fmt-check also format the markdown with prettier, pinned in yarn.lock. The agent guidance moves to AGENTS.md. The Go code is fixed for what the new configuration finds, and the AS and prefix detail reads now return the rows' error after their loops. Model: opus-5-5
158 lines
4.2 KiB
Go
158 lines
4.2 KiB
Go
// Package config provides centralized configuration management for RouteWatch
|
|
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// AppIdentifier is the reverse domain name identifier for the app
|
|
AppIdentifier = "berlin.sneak.app.routewatch"
|
|
|
|
// dirPermissions for creating directories
|
|
dirPermissions = 0750 // rwxr-x---
|
|
|
|
// defaultRouteExpirationMinutes is the default route expiration timeout in minutes
|
|
defaultRouteExpirationMinutes = 5
|
|
|
|
// defaultPort is the HTTP port used when PORT is not set
|
|
defaultPort = 8080
|
|
|
|
// maxPort is the highest TCP port number
|
|
maxPort = 65535
|
|
)
|
|
|
|
var (
|
|
errRelativeXDGDataHome = errors.New("XDG_DATA_HOME must be an absolute path")
|
|
errUnsupportedOS = errors.New("unsupported operating system")
|
|
errInvalidPort = errors.New("PORT must be a whole number")
|
|
)
|
|
|
|
// Config holds configuration for the entire application
|
|
type Config struct {
|
|
// StateDir is the directory for all application state (database, snapshots)
|
|
StateDir string
|
|
|
|
// Port is the TCP port the HTTP server listens on
|
|
Port int
|
|
|
|
// MaxRuntime is the maximum runtime (0 = run forever)
|
|
MaxRuntime time.Duration
|
|
|
|
// EnableBatchedDatabaseWrites enables batched database operations for
|
|
// better performance
|
|
EnableBatchedDatabaseWrites bool
|
|
|
|
// RouteExpirationTimeout is how long a route can go without being
|
|
// refreshed before expiring
|
|
// Default is 2 hours which is conservative for BGP (typical BGP hold
|
|
// time is 90-180 seconds)
|
|
RouteExpirationTimeout time.Duration
|
|
}
|
|
|
|
// New creates a new Config with default paths based on the OS
|
|
func New() (*Config, error) {
|
|
stateDir, err := getStateDirectory()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to determine state directory: %w", err)
|
|
}
|
|
|
|
port, err := getPort()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Config{
|
|
StateDir: stateDir,
|
|
Port: port,
|
|
// Run forever by default
|
|
MaxRuntime: 0,
|
|
// Enable batching by default
|
|
EnableBatchedDatabaseWrites: true,
|
|
// For active route monitoring
|
|
RouteExpirationTimeout: defaultRouteExpirationMinutes * time.Minute,
|
|
}, nil
|
|
}
|
|
|
|
// GetStateDir returns the state directory path
|
|
func (c *Config) GetStateDir() string {
|
|
return c.StateDir
|
|
}
|
|
|
|
// getStateDirectory returns the appropriate state directory based on the OS
|
|
func getStateDirectory() (string, error) {
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
// macOS: ~/Library/Application Support/berlin.sneak.app.routewatch
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return filepath.Join(home, "Library", "Application Support", AppIdentifier), nil
|
|
|
|
case "linux", "freebsd", "openbsd", "netbsd":
|
|
// The XDG spec requires an absolute path; a relative one would put
|
|
// the database somewhere unexpected.
|
|
xdgData := os.Getenv("XDG_DATA_HOME")
|
|
if xdgData != "" && !filepath.IsAbs(xdgData) {
|
|
return "", fmt.Errorf("%w, got %q", errRelativeXDGDataHome, xdgData)
|
|
}
|
|
|
|
// Unix-like: /var/lib/berlin.sneak.app.routewatch if root, else XDG_DATA_HOME
|
|
if os.Geteuid() == 0 {
|
|
return filepath.Join("/var/lib", AppIdentifier), nil
|
|
}
|
|
|
|
// Check XDG_DATA_HOME first
|
|
if xdgData != "" {
|
|
return filepath.Join(xdgData, AppIdentifier), nil
|
|
}
|
|
|
|
// Fall back to ~/.local/share/berlin.sneak.app.routewatch
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return filepath.Join(home, ".local", "share", AppIdentifier), nil
|
|
|
|
default:
|
|
return "", fmt.Errorf("%w: %s", errUnsupportedOS, runtime.GOOS)
|
|
}
|
|
}
|
|
|
|
// getPort returns the HTTP port from PORT, or defaultPort when PORT is not set
|
|
func getPort() (int, error) {
|
|
value := os.Getenv("PORT")
|
|
if value == "" {
|
|
return defaultPort, nil
|
|
}
|
|
|
|
// ParseUint, unlike Atoi, refuses a sign: the health check URL cannot use "+9090"
|
|
port, err := strconv.ParseUint(value, 10, 0)
|
|
if err != nil || port < 1 || port > maxPort {
|
|
return 0, fmt.Errorf("%w from 1 to %d, got %q",
|
|
errInvalidPort, maxPort, value)
|
|
}
|
|
|
|
return int(port), nil
|
|
}
|
|
|
|
// EnsureDirectories creates all necessary directories if they don't exist
|
|
func (c *Config) EnsureDirectories() error {
|
|
// Ensure state directory exists
|
|
err := os.MkdirAll(c.StateDir, dirPermissions)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create state directory: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|