Implement IP API daemon with GeoIP database support
- Create modular architecture with separate packages for config, database, HTTP, logging, and state management - Implement Cobra CLI with daemon command - Set up Uber FX dependency injection - Add Chi router with health check and IP lookup endpoints - Implement GeoIP database downloader with automatic updates - Add state persistence for tracking database download times - Include comprehensive test coverage for all components - Configure structured logging with slog - Add Makefile with test, lint, and build targets - Support both IPv4 and IPv6 lookups - Return country, city, ASN, and location data in JSON format
This commit is contained in:
77
internal/config/config.go
Normal file
77
internal/config/config.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Package config handles application configuration.
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"git.eeqj.de/sneak/smartconfig"
|
||||
)
|
||||
|
||||
// Config holds the application configuration.
|
||||
type Config struct {
|
||||
Port int
|
||||
StateDir string
|
||||
LogLevel string
|
||||
}
|
||||
|
||||
// New creates a new configuration instance.
|
||||
func New(configFile string) (*Config, error) {
|
||||
// Check if config file exists first
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
return newDefaultConfig(), nil
|
||||
}
|
||||
|
||||
// Load smartconfig
|
||||
sc, err := smartconfig.NewFromConfigPath(configFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := &Config{}
|
||||
|
||||
// Get port from smartconfig or environment or default
|
||||
if port, err := sc.GetInt("port"); err == nil {
|
||||
cfg.Port = port
|
||||
} else {
|
||||
cfg.Port = getPortFromEnv()
|
||||
}
|
||||
|
||||
// Get state directory
|
||||
if stateDir, err := sc.GetString("state_dir"); err == nil {
|
||||
cfg.StateDir = stateDir
|
||||
} else {
|
||||
cfg.StateDir = "/var/lib/ipapi"
|
||||
}
|
||||
|
||||
// Get log level
|
||||
if logLevel, err := sc.GetString("log_level"); err == nil {
|
||||
cfg.LogLevel = logLevel
|
||||
} else {
|
||||
cfg.LogLevel = "info"
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func newDefaultConfig() *Config {
|
||||
return &Config{
|
||||
Port: getPortFromEnv(),
|
||||
StateDir: "/var/lib/ipapi",
|
||||
LogLevel: "info",
|
||||
}
|
||||
}
|
||||
|
||||
func getPortFromEnv() int {
|
||||
const defaultPort = 8080
|
||||
portStr := os.Getenv("PORT")
|
||||
if portStr == "" {
|
||||
return defaultPort
|
||||
}
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return defaultPort
|
||||
}
|
||||
|
||||
return port
|
||||
}
|
||||
64
internal/config/config_test.go
Normal file
64
internal/config/config_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewDefaultConfig(t *testing.T) {
|
||||
// Clear PORT env var for test
|
||||
oldPort := os.Getenv("PORT")
|
||||
os.Unsetenv("PORT")
|
||||
defer os.Setenv("PORT", oldPort)
|
||||
|
||||
cfg := newDefaultConfig()
|
||||
if cfg.Port != 8080 {
|
||||
t.Errorf("expected default port 8080, got %d", cfg.Port)
|
||||
}
|
||||
if cfg.StateDir != "/var/lib/ipapi" {
|
||||
t.Errorf("expected default state dir /var/lib/ipapi, got %s", cfg.StateDir)
|
||||
}
|
||||
if cfg.LogLevel != "info" {
|
||||
t.Errorf("expected default log level info, got %s", cfg.LogLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPortFromEnv(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
envValue string
|
||||
expected int
|
||||
}{
|
||||
{"no env", "", 8080},
|
||||
{"valid port", "9090", 9090},
|
||||
{"invalid port", "invalid", 8080},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
oldPort := os.Getenv("PORT")
|
||||
if tt.envValue == "" {
|
||||
os.Unsetenv("PORT")
|
||||
} else {
|
||||
os.Setenv("PORT", tt.envValue)
|
||||
}
|
||||
defer os.Setenv("PORT", oldPort)
|
||||
|
||||
port := getPortFromEnv()
|
||||
if port != tt.expected {
|
||||
t.Errorf("expected port %d, got %d", tt.expected, port)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
// Test with non-existent file (should use defaults)
|
||||
cfg, err := New("/nonexistent/config.yml")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error for non-existent file, got %v", err)
|
||||
}
|
||||
if cfg == nil {
|
||||
t.Fatal("expected config, got nil")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user