fix(go.sum): update ResolveSpec dependency to v1.0.87
CI / build-and-test (push) Failing after 1s
Release / release (push) Failing after 19m26s

This commit is contained in:
Hein
2026-06-23 13:17:16 +02:00
parent 0227912325
commit 1adf50e3db
2436 changed files with 1078758 additions and 114 deletions
+291
View File
@@ -0,0 +1,291 @@
# ResolveSpec Configuration System
A centralized configuration system with support for multiple configuration sources: config files (YAML, TOML, JSON), environment variables, and programmatic configuration.
## Features
- **Multiple Config Sources**: Config files, environment variables, and code
- **Priority Order**: Environment variables > Config file > Defaults
- **Multiple Formats**: YAML, TOML, JSON supported
- **Type Safety**: Strongly-typed configuration structs
- **Sensible Defaults**: Works out of the box with reasonable defaults
## Quick Start
### Basic Usage
```go
import "github.com/heinhel/ResolveSpec/pkg/config"
// Create a new config manager
mgr := config.NewManager()
// Load configuration from file and environment
if err := mgr.Load(); err != nil {
log.Fatal(err)
}
// Get the complete configuration
cfg, err := mgr.GetConfig()
if err != nil {
log.Fatal(err)
}
// Use the configuration
fmt.Println("Server address:", cfg.Server.Addr)
```
### Custom Configuration Paths
```go
mgr := config.NewManagerWithOptions(
config.WithConfigFile("/path/to/config.yaml"),
config.WithEnvPrefix("MYAPP"),
)
```
## Configuration Sources
### 1. Config Files
Place a `config.yaml` file in one of these locations:
- Current directory (`.`)
- `./config/`
- `/etc/resolvespec/`
- `$HOME/.resolvespec/`
Example `config.yaml`:
```yaml
server:
addr: ":8080"
shutdown_timeout: 30s
tracing:
enabled: true
service_name: "my-service"
cache:
provider: "redis"
redis:
host: "localhost"
port: 6379
```
### 2. Environment Variables
All configuration can be set via environment variables with the `RESOLVESPEC_` prefix:
```bash
export RESOLVESPEC_SERVER_ADDR=":9090"
export RESOLVESPEC_TRACING_ENABLED=true
export RESOLVESPEC_CACHE_PROVIDER=redis
export RESOLVESPEC_CACHE_REDIS_HOST=localhost
```
Nested configuration uses underscores:
- `server.addr``RESOLVESPEC_SERVER_ADDR`
- `cache.redis.host``RESOLVESPEC_CACHE_REDIS_HOST`
### 3. Programmatic Configuration
```go
mgr := config.NewManager()
mgr.Set("server.addr", ":9090")
mgr.Set("tracing.enabled", true)
cfg, _ := mgr.GetConfig()
```
## Configuration Options
### Server Configuration
```yaml
server:
addr: ":8080" # Server address
shutdown_timeout: 30s # Graceful shutdown timeout
drain_timeout: 25s # Connection drain timeout
read_timeout: 10s # HTTP read timeout
write_timeout: 10s # HTTP write timeout
idle_timeout: 120s # HTTP idle timeout
```
### Tracing Configuration
```yaml
tracing:
enabled: false # Enable/disable tracing
service_name: "resolvespec" # Service name
service_version: "1.0.0" # Service version
endpoint: "http://localhost:4318/v1/traces" # OTLP endpoint
```
### Cache Configuration
```yaml
cache:
provider: "memory" # Options: memory, redis, memcache
redis:
host: "localhost"
port: 6379
password: ""
db: 0
memcache:
servers:
- "localhost:11211"
max_idle_conns: 10
timeout: 100ms
```
### Logger Configuration
```yaml
logger:
dev: false # Development mode (human-readable output)
path: "" # Log file path (empty = stdout)
```
### Middleware Configuration
```yaml
middleware:
rate_limit_rps: 100.0 # Requests per second
rate_limit_burst: 200 # Burst size
max_request_size: 10485760 # Max request size in bytes (10MB)
```
### CORS Configuration
```yaml
cors:
allowed_origins:
- "*"
allowed_methods:
- "GET"
- "POST"
- "PUT"
- "DELETE"
- "OPTIONS"
allowed_headers:
- "*"
max_age: 3600
```
### Database Configuration
```yaml
database:
url: "host=localhost user=postgres password=postgres dbname=mydb port=5432 sslmode=disable"
```
## Priority and Overrides
Configuration sources are applied in this order (highest priority first):
1. **Environment Variables** (highest priority)
2. **Config File**
3. **Defaults** (lowest priority)
This allows you to:
- Set defaults in code
- Override with a config file
- Override specific values with environment variables
## Examples
### Production Setup
```yaml
# config.yaml
server:
addr: ":8080"
tracing:
enabled: true
service_name: "myapi"
endpoint: "http://jaeger:4318/v1/traces"
cache:
provider: "redis"
redis:
host: "redis"
port: 6379
password: "${REDIS_PASSWORD}"
logger:
dev: false
path: "/var/log/myapi/app.log"
```
### Development Setup
```bash
# Use environment variables for development
export RESOLVESPEC_LOGGER_DEV=true
export RESOLVESPEC_TRACING_ENABLED=false
export RESOLVESPEC_CACHE_PROVIDER=memory
```
### Testing Setup
```go
// Override config for tests
mgr := config.NewManager()
mgr.Set("cache.provider", "memory")
mgr.Set("database.url", testDBURL)
cfg, _ := mgr.GetConfig()
```
## Best Practices
1. **Use config files for base configuration** - Define your standard settings
2. **Use environment variables for secrets** - Never commit passwords/tokens
3. **Use environment variables for deployment-specific values** - Different per environment
4. **Keep defaults sensible** - Application should work with minimal configuration
5. **Document your configuration** - Comment your config.yaml files
## Integration with ResolveSpec Components
The configuration system integrates seamlessly with ResolveSpec components:
```go
cfg, _ := config.NewManager().Load().GetConfig()
// Server
srv := server.NewGracefulServer(server.Config{
Addr: cfg.Server.Addr,
ShutdownTimeout: cfg.Server.ShutdownTimeout,
// ... other fields
})
// Tracing
if cfg.Tracing.Enabled {
tracer := tracing.Init(tracing.Config{
ServiceName: cfg.Tracing.ServiceName,
ServiceVersion: cfg.Tracing.ServiceVersion,
Endpoint: cfg.Tracing.Endpoint,
})
defer tracer.Shutdown(context.Background())
}
// Cache
var cacheProvider cache.Provider
switch cfg.Cache.Provider {
case "redis":
cacheProvider = cache.NewRedisProvider(cfg.Cache.Redis.Host, cfg.Cache.Redis.Port, ...)
case "memcache":
cacheProvider = cache.NewMemcacheProvider(cfg.Cache.Memcache.Servers, ...)
default:
cacheProvider = cache.NewMemoryProvider()
}
// Logger
logger.Init(cfg.Logger.Dev)
if cfg.Logger.Path != "" {
logger.UpdateLoggerPath(cfg.Logger.Path, cfg.Logger.Dev)
}
```
+196
View File
@@ -0,0 +1,196 @@
package config
import "time"
// Config represents the complete application configuration
type Config struct {
Servers ServersConfig `mapstructure:"servers"`
Tracing TracingConfig `mapstructure:"tracing"`
Cache CacheConfig `mapstructure:"cache"`
Logger LoggerConfig `mapstructure:"logger"`
ErrorTracking ErrorTrackingConfig `mapstructure:"error_tracking"`
Middleware MiddlewareConfig `mapstructure:"middleware"`
CORS CORSConfig `mapstructure:"cors"`
EventBroker EventBrokerConfig `mapstructure:"event_broker"`
DBManager DBManagerConfig `mapstructure:"dbmanager"`
Paths PathsConfig `mapstructure:"paths"`
Extensions map[string]interface{} `mapstructure:"extensions"`
}
// ServersConfig contains configuration for the server manager
type ServersConfig struct {
// DefaultServer is the name of the default server to use
DefaultServer string `mapstructure:"default_server"`
// Instances is a map of server name to server configuration
Instances map[string]ServerInstanceConfig `mapstructure:"instances"`
// Global timeout defaults (can be overridden per instance)
ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"`
DrainTimeout time.Duration `mapstructure:"drain_timeout"`
ReadTimeout time.Duration `mapstructure:"read_timeout"`
WriteTimeout time.Duration `mapstructure:"write_timeout"`
IdleTimeout time.Duration `mapstructure:"idle_timeout"`
}
// ServerInstanceConfig defines configuration for a single server instance
type ServerInstanceConfig struct {
// Name is the unique name of this server instance
Name string `mapstructure:"name"`
// Host is the host to bind to (e.g., "localhost", "0.0.0.0", "")
Host string `mapstructure:"host"`
// Port is the port number to listen on
Port int `mapstructure:"port"`
// Description is a human-readable description of this server
Description string `mapstructure:"description"`
// GZIP enables GZIP compression middleware
GZIP bool `mapstructure:"gzip"`
// TLS/HTTPS configuration options (mutually exclusive)
// Option 1: Provide certificate and key files directly
SSLCert string `mapstructure:"ssl_cert"`
SSLKey string `mapstructure:"ssl_key"`
// Option 2: Use self-signed certificate (for development/testing)
SelfSignedSSL bool `mapstructure:"self_signed_ssl"`
// Option 3: Use Let's Encrypt / AutoTLS
AutoTLS bool `mapstructure:"auto_tls"`
AutoTLSDomains []string `mapstructure:"auto_tls_domains"`
AutoTLSCacheDir string `mapstructure:"auto_tls_cache_dir"`
AutoTLSEmail string `mapstructure:"auto_tls_email"`
// Timeout configurations (overrides global defaults)
ShutdownTimeout *time.Duration `mapstructure:"shutdown_timeout"`
DrainTimeout *time.Duration `mapstructure:"drain_timeout"`
ReadTimeout *time.Duration `mapstructure:"read_timeout"`
WriteTimeout *time.Duration `mapstructure:"write_timeout"`
IdleTimeout *time.Duration `mapstructure:"idle_timeout"`
// Tags for organization and filtering
Tags map[string]string `mapstructure:"tags"`
// ExternalURLs are additional URLs that this server instance is accessible from (for CORS) for proxy setups
ExternalURLs []string `mapstructure:"external_urls"`
}
// TracingConfig holds OpenTelemetry tracing configuration
type TracingConfig struct {
Enabled bool `mapstructure:"enabled"`
ServiceName string `mapstructure:"service_name"`
ServiceVersion string `mapstructure:"service_version"`
Endpoint string `mapstructure:"endpoint"`
}
// CacheConfig holds cache provider configuration
type CacheConfig struct {
Provider string `mapstructure:"provider"` // memory, redis, memcache
Redis RedisConfig `mapstructure:"redis"`
Memcache MemcacheConfig `mapstructure:"memcache"`
}
// RedisConfig holds Redis-specific configuration
type RedisConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
// MemcacheConfig holds Memcache-specific configuration
type MemcacheConfig struct {
Servers []string `mapstructure:"servers"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
Timeout time.Duration `mapstructure:"timeout"`
}
// LoggerConfig holds logger configuration
type LoggerConfig struct {
Dev bool `mapstructure:"dev"`
Path string `mapstructure:"path"`
}
// MiddlewareConfig holds middleware configuration
type MiddlewareConfig struct {
RateLimitRPS float64 `mapstructure:"rate_limit_rps"`
RateLimitBurst int `mapstructure:"rate_limit_burst"`
MaxRequestSize int64 `mapstructure:"max_request_size"`
}
// CORSConfig holds CORS configuration
type CORSConfig struct {
AllowedOrigins []string `mapstructure:"allowed_origins"`
AllowedMethods []string `mapstructure:"allowed_methods"`
AllowedHeaders []string `mapstructure:"allowed_headers"`
MaxAge int `mapstructure:"max_age"`
}
// ErrorTrackingConfig holds error tracking configuration
type ErrorTrackingConfig struct {
Enabled bool `mapstructure:"enabled"`
Provider string `mapstructure:"provider"` // sentry, noop
DSN string `mapstructure:"dsn"` // Sentry DSN
Environment string `mapstructure:"environment"` // e.g., production, staging, development
Release string `mapstructure:"release"` // Application version/release
Debug bool `mapstructure:"debug"` // Enable debug mode
SampleRate float64 `mapstructure:"sample_rate"` // Error sample rate (0.0-1.0)
TracesSampleRate float64 `mapstructure:"traces_sample_rate"` // Traces sample rate (0.0-1.0)
}
// EventBrokerConfig contains configuration for the event broker
type EventBrokerConfig struct {
Enabled bool `mapstructure:"enabled"`
Provider string `mapstructure:"provider"` // memory, redis, nats, database
Mode string `mapstructure:"mode"` // sync, async
WorkerCount int `mapstructure:"worker_count"`
BufferSize int `mapstructure:"buffer_size"`
InstanceID string `mapstructure:"instance_id"`
Redis EventBrokerRedisConfig `mapstructure:"redis"`
NATS EventBrokerNATSConfig `mapstructure:"nats"`
Database EventBrokerDatabaseConfig `mapstructure:"database"`
RetryPolicy EventBrokerRetryPolicyConfig `mapstructure:"retry_policy"`
}
// EventBrokerRedisConfig contains Redis-specific configuration
type EventBrokerRedisConfig struct {
StreamName string `mapstructure:"stream_name"`
ConsumerGroup string `mapstructure:"consumer_group"`
MaxLen int64 `mapstructure:"max_len"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
// EventBrokerNATSConfig contains NATS-specific configuration
type EventBrokerNATSConfig struct {
URL string `mapstructure:"url"`
StreamName string `mapstructure:"stream_name"`
Subjects []string `mapstructure:"subjects"`
Storage string `mapstructure:"storage"` // file, memory
MaxAge time.Duration `mapstructure:"max_age"`
}
// EventBrokerDatabaseConfig contains database provider configuration
type EventBrokerDatabaseConfig struct {
TableName string `mapstructure:"table_name"`
Channel string `mapstructure:"channel"` // PostgreSQL NOTIFY channel name
PollInterval time.Duration `mapstructure:"poll_interval"`
}
// EventBrokerRetryPolicyConfig contains retry policy configuration
type EventBrokerRetryPolicyConfig struct {
MaxRetries int `mapstructure:"max_retries"`
InitialDelay time.Duration `mapstructure:"initial_delay"`
MaxDelay time.Duration `mapstructure:"max_delay"`
BackoffFactor float64 `mapstructure:"backoff_factor"`
}
// PathsConfig contains configuration for named file system paths
// This is a map of path name to file system path
// Example: "data_dir": "/var/lib/myapp/data"
type PathsConfig map[string]string
+264
View File
@@ -0,0 +1,264 @@
package config
import (
"fmt"
"net/url"
"strconv"
"strings"
"time"
)
// DBManagerConfig contains configuration for the database connection manager
type DBManagerConfig struct {
// DefaultConnection is the name of the default connection to use
DefaultConnection string `mapstructure:"default_connection"`
// Connections is a map of connection name to connection configuration
Connections map[string]DBConnectionConfig `mapstructure:"connections"`
// Global connection pool defaults
MaxOpenConns int `mapstructure:"max_open_conns"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"`
// Retry policy
RetryAttempts int `mapstructure:"retry_attempts"`
RetryDelay time.Duration `mapstructure:"retry_delay"`
RetryMaxDelay time.Duration `mapstructure:"retry_max_delay"`
// Health checks
HealthCheckInterval time.Duration `mapstructure:"health_check_interval"`
EnableAutoReconnect bool `mapstructure:"enable_auto_reconnect"`
}
// DBConnectionConfig defines configuration for a single database connection
type DBConnectionConfig struct {
// Name is the unique name of this connection
Name string `mapstructure:"name"`
// Type is the database type (postgres, sqlite, mssql, mongodb)
Type string `mapstructure:"type"`
// DSN is the complete Data Source Name / connection string
// If provided, this takes precedence over individual connection parameters
DSN string `mapstructure:"dsn"`
// Connection parameters (used if DSN is not provided)
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Database string `mapstructure:"database"`
// PostgreSQL/MSSQL specific
SSLMode string `mapstructure:"sslmode"` // disable, require, verify-ca, verify-full
Schema string `mapstructure:"schema"` // Default schema
// SQLite specific
FilePath string `mapstructure:"filepath"`
// MongoDB specific
AuthSource string `mapstructure:"auth_source"`
ReplicaSet string `mapstructure:"replica_set"`
ReadPreference string `mapstructure:"read_preference"` // primary, secondary, etc.
// Connection pool settings (overrides global defaults)
MaxOpenConns *int `mapstructure:"max_open_conns"`
MaxIdleConns *int `mapstructure:"max_idle_conns"`
ConnMaxLifetime *time.Duration `mapstructure:"conn_max_lifetime"`
ConnMaxIdleTime *time.Duration `mapstructure:"conn_max_idle_time"`
// Timeouts
ConnectTimeout time.Duration `mapstructure:"connect_timeout"`
QueryTimeout time.Duration `mapstructure:"query_timeout"`
// Features
EnableTracing bool `mapstructure:"enable_tracing"`
EnableMetrics bool `mapstructure:"enable_metrics"`
EnableLogging bool `mapstructure:"enable_logging"`
// DefaultORM specifies which ORM to use for the Database() method
// Options: "bun", "gorm", "native"
DefaultORM string `mapstructure:"default_orm"`
// Tags for organization and filtering
Tags map[string]string `mapstructure:"tags"`
}
// ToManagerConfig converts config.DBManagerConfig to dbmanager.ManagerConfig
// This is used to avoid circular dependencies
func (c *DBManagerConfig) ToManagerConfig() interface{} {
// This will be implemented in the dbmanager package
// to convert from config types to dbmanager types
return c
}
// PopulateFromDSN parses a DSN and populates the connection fields
func (cc *DBConnectionConfig) PopulateFromDSN() error {
if cc.DSN == "" {
return nil // Nothing to populate
}
switch cc.Type {
case "postgres":
return cc.populatePostgresDSN()
case "mongodb":
return cc.populateMongoDSN()
case "mssql":
return cc.populateMSSQLDSN()
case "sqlite":
return cc.populateSQLiteDSN()
default:
return fmt.Errorf("cannot parse DSN for unsupported database type: %s", cc.Type)
}
}
// populatePostgresDSN parses PostgreSQL DSN format
// Example: host=localhost port=5432 user=postgres password=secret dbname=mydb sslmode=disable
func (cc *DBConnectionConfig) populatePostgresDSN() error {
parts := strings.Fields(cc.DSN)
for _, part := range parts {
kv := strings.SplitN(part, "=", 2)
if len(kv) != 2 {
continue
}
key, value := kv[0], kv[1]
switch key {
case "host":
cc.Host = value
case "port":
port, err := strconv.Atoi(value)
if err != nil {
return fmt.Errorf("invalid port in DSN: %w", err)
}
cc.Port = port
case "user":
cc.User = value
case "password":
cc.Password = value
case "dbname":
cc.Database = value
case "sslmode":
cc.SSLMode = value
case "search_path":
cc.Schema = value
}
}
return nil
}
// populateMongoDSN parses MongoDB DSN format
// Example: mongodb://user:password@host:port/database?authSource=admin&replicaSet=rs0
func (cc *DBConnectionConfig) populateMongoDSN() error {
u, err := url.Parse(cc.DSN)
if err != nil {
return fmt.Errorf("invalid MongoDB DSN: %w", err)
}
// Extract user and password
if u.User != nil {
cc.User = u.User.Username()
if password, ok := u.User.Password(); ok {
cc.Password = password
}
}
// Extract host and port
if u.Host != "" {
host := u.Host
if strings.Contains(host, ":") {
hostPort := strings.SplitN(host, ":", 2)
cc.Host = hostPort[0]
if port, err := strconv.Atoi(hostPort[1]); err == nil {
cc.Port = port
}
} else {
cc.Host = host
}
}
// Extract database
if u.Path != "" {
cc.Database = strings.TrimPrefix(u.Path, "/")
}
// Extract query parameters
params := u.Query()
if authSource := params.Get("authSource"); authSource != "" {
cc.AuthSource = authSource
}
if replicaSet := params.Get("replicaSet"); replicaSet != "" {
cc.ReplicaSet = replicaSet
}
if readPref := params.Get("readPreference"); readPref != "" {
cc.ReadPreference = readPref
}
return nil
}
// populateMSSQLDSN parses MSSQL DSN format
// Example: sqlserver://username:password@host:port?database=dbname&schema=dbo
func (cc *DBConnectionConfig) populateMSSQLDSN() error {
u, err := url.Parse(cc.DSN)
if err != nil {
return fmt.Errorf("invalid MSSQL DSN: %w", err)
}
// Extract user and password
if u.User != nil {
cc.User = u.User.Username()
if password, ok := u.User.Password(); ok {
cc.Password = password
}
}
// Extract host and port
if u.Host != "" {
host := u.Host
if strings.Contains(host, ":") {
hostPort := strings.SplitN(host, ":", 2)
cc.Host = hostPort[0]
if port, err := strconv.Atoi(hostPort[1]); err == nil {
cc.Port = port
}
} else {
cc.Host = host
}
}
// Extract query parameters
params := u.Query()
if database := params.Get("database"); database != "" {
cc.Database = database
}
if schema := params.Get("schema"); schema != "" {
cc.Schema = schema
}
return nil
}
// populateSQLiteDSN parses SQLite DSN format
// Example: /path/to/database.db or :memory:
func (cc *DBConnectionConfig) populateSQLiteDSN() error {
cc.FilePath = cc.DSN
return nil
}
// Validate validates the DBManager configuration
func (c *DBManagerConfig) Validate() error {
if len(c.Connections) == 0 {
return fmt.Errorf("at least one connection must be configured")
}
if c.DefaultConnection != "" {
if _, ok := c.Connections[c.DefaultConnection]; !ok {
return fmt.Errorf("default connection '%s' not found in connections", c.DefaultConnection)
}
}
return nil
}
+293
View File
@@ -0,0 +1,293 @@
package config
import (
"fmt"
"strings"
"github.com/spf13/viper"
)
// Manager handles configuration loading from multiple sources
type Manager struct {
v *viper.Viper
}
var configInstance *Manager
// GetConfigManager returns a singleton configuration manager instance
func GetConfigManager() *Manager {
if configInstance == nil {
configInstance = NewManager()
}
return configInstance
}
// NewManager creates a new configuration manager with defaults
func NewManager() *Manager {
v := viper.New()
// Set configuration file settings
v.SetConfigName("config")
v.SetConfigType("yaml")
v.AddConfigPath(".")
v.AddConfigPath("./config")
v.AddConfigPath("/etc/resolvespec")
v.AddConfigPath("$HOME/.resolvespec")
// Enable environment variable support
v.SetEnvPrefix("RESOLVESPEC")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
// Set default values
setDefaults(v)
configInstance = &Manager{v: v}
return configInstance
}
// NewManagerWithOptions creates a new configuration manager with custom options
func NewManagerWithOptions(opts ...Option) *Manager {
m := NewManager()
for _, opt := range opts {
opt(m)
}
return m
}
// Option is a functional option for configuring the Manager
type Option func(*Manager)
// WithConfigFile sets a specific config file path
func WithConfigFile(path string) Option {
return func(m *Manager) {
m.v.SetConfigFile(path)
}
}
// WithConfigName sets the config file name (without extension)
func WithConfigName(name string) Option {
return func(m *Manager) {
m.v.SetConfigName(name)
}
}
// WithConfigPath adds a path to search for config files
func WithConfigPath(path string) Option {
return func(m *Manager) {
m.v.AddConfigPath(path)
}
}
// WithEnvPrefix sets the environment variable prefix
func WithEnvPrefix(prefix string) Option {
return func(m *Manager) {
m.v.SetEnvPrefix(prefix)
}
}
// Load attempts to load configuration from file and environment
func (m *Manager) Load() error {
// Try to read config file (not an error if it doesn't exist)
if err := m.v.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return fmt.Errorf("error reading config file: %w", err)
}
// Config file not found; will rely on defaults and env vars
}
return nil
}
// GetConfig returns the complete configuration
func (m *Manager) GetConfig() (*Config, error) {
var cfg Config
if err := m.v.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
return &cfg, nil
}
// SetConfig sets the complete configuration
func (m *Manager) SetConfig(cfg *Config) error {
configMap := make(map[string]interface{})
// Marshal the config to a map structure that viper can use
if err := m.v.Unmarshal(&configMap); err != nil {
return fmt.Errorf("failed to prepare config map: %w", err)
}
// Use viper's merge to apply the config
m.v.Set("servers", cfg.Servers)
m.v.Set("tracing", cfg.Tracing)
m.v.Set("cache", cfg.Cache)
m.v.Set("logger", cfg.Logger)
m.v.Set("error_tracking", cfg.ErrorTracking)
m.v.Set("middleware", cfg.Middleware)
m.v.Set("cors", cfg.CORS)
m.v.Set("event_broker", cfg.EventBroker)
m.v.Set("dbmanager", cfg.DBManager)
m.v.Set("paths", cfg.Paths)
m.v.Set("extensions", cfg.Extensions)
return nil
}
// Get returns a configuration value by key
func (m *Manager) Get(key string) interface{} {
return m.v.Get(key)
}
// GetString returns a string configuration value
func (m *Manager) GetString(key string) string {
return m.v.GetString(key)
}
// GetInt returns an int configuration value
func (m *Manager) GetInt(key string) int {
return m.v.GetInt(key)
}
// GetBool returns a bool configuration value
func (m *Manager) GetBool(key string) bool {
return m.v.GetBool(key)
}
// Set sets a configuration value
func (m *Manager) Set(key string, value interface{}) {
m.v.Set(key, value)
}
// SaveConfig writes the current configuration to the specified path
func (m *Manager) SaveConfig(path string) error {
if err := m.v.WriteConfigAs(path); err != nil {
return fmt.Errorf("failed to save config to %s: %w", path, err)
}
return nil
}
// setDefaults sets default configuration values
func setDefaults(v *viper.Viper) {
// Server defaults - new structure
v.SetDefault("servers.default_server", "default")
// Global server timeout defaults
v.SetDefault("servers.shutdown_timeout", "30s")
v.SetDefault("servers.drain_timeout", "25s")
v.SetDefault("servers.read_timeout", "10s")
v.SetDefault("servers.write_timeout", "10s")
v.SetDefault("servers.idle_timeout", "120s")
// Default server instance
v.SetDefault("servers.instances.default.name", "default")
v.SetDefault("servers.instances.default.host", "")
v.SetDefault("servers.instances.default.port", 8080)
v.SetDefault("servers.instances.default.description", "Default HTTP server")
v.SetDefault("servers.instances.default.gzip", false)
// Tracing defaults
v.SetDefault("tracing.enabled", false)
v.SetDefault("tracing.service_name", "resolvespec")
v.SetDefault("tracing.service_version", "1.0.0")
v.SetDefault("tracing.endpoint", "")
// Cache defaults
v.SetDefault("cache.provider", "memory")
v.SetDefault("cache.redis.host", "localhost")
v.SetDefault("cache.redis.port", 6379)
v.SetDefault("cache.redis.password", "")
v.SetDefault("cache.redis.db", 0)
v.SetDefault("cache.memcache.servers", []string{"localhost:11211"})
v.SetDefault("cache.memcache.max_idle_conns", 10)
v.SetDefault("cache.memcache.timeout", "100ms")
// Logger defaults
v.SetDefault("logger.dev", false)
v.SetDefault("logger.path", "")
// Middleware defaults
v.SetDefault("middleware.rate_limit_rps", 100.0)
v.SetDefault("middleware.rate_limit_burst", 200)
v.SetDefault("middleware.max_request_size", 10485760) // 10MB
// CORS defaults
v.SetDefault("cors.allowed_origins", []string{"*"})
v.SetDefault("cors.allowed_methods", []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"})
v.SetDefault("cors.allowed_headers", []string{"*"})
v.SetDefault("cors.max_age", 3600)
// Database defaults
v.SetDefault("database.url", "")
// Database Manager defaults
v.SetDefault("dbmanager.default_connection", "default")
v.SetDefault("dbmanager.max_open_conns", 25)
v.SetDefault("dbmanager.max_idle_conns", 5)
v.SetDefault("dbmanager.conn_max_lifetime", "30m")
v.SetDefault("dbmanager.conn_max_idle_time", "5m")
v.SetDefault("dbmanager.retry_attempts", 3)
v.SetDefault("dbmanager.retry_delay", "1s")
v.SetDefault("dbmanager.retry_max_delay", "10s")
v.SetDefault("dbmanager.health_check_interval", "30s")
v.SetDefault("dbmanager.enable_auto_reconnect", true)
// Default PostgreSQL connection
v.SetDefault("dbmanager.connections.default.name", "default")
v.SetDefault("dbmanager.connections.default.type", "postgres")
v.SetDefault("dbmanager.connections.default.host", "localhost")
v.SetDefault("dbmanager.connections.default.port", 5432)
v.SetDefault("dbmanager.connections.default.user", "postgres")
v.SetDefault("dbmanager.connections.default.password", "")
v.SetDefault("dbmanager.connections.default.database", "resolvespec")
v.SetDefault("dbmanager.connections.default.sslmode", "disable")
v.SetDefault("dbmanager.connections.default.connect_timeout", "10s")
v.SetDefault("dbmanager.connections.default.query_timeout", "30s")
v.SetDefault("dbmanager.connections.default.enable_tracing", false)
v.SetDefault("dbmanager.connections.default.enable_metrics", false)
v.SetDefault("dbmanager.connections.default.enable_logging", false)
v.SetDefault("dbmanager.connections.default.default_orm", "bun")
// Event Broker defaults
v.SetDefault("event_broker.enabled", false)
v.SetDefault("event_broker.provider", "memory")
v.SetDefault("event_broker.mode", "async")
v.SetDefault("event_broker.worker_count", 10)
v.SetDefault("event_broker.buffer_size", 1000)
v.SetDefault("event_broker.instance_id", "")
// Event Broker - Redis defaults
v.SetDefault("event_broker.redis.stream_name", "resolvespec:events")
v.SetDefault("event_broker.redis.consumer_group", "resolvespec-workers")
v.SetDefault("event_broker.redis.max_len", 10000)
v.SetDefault("event_broker.redis.host", "localhost")
v.SetDefault("event_broker.redis.port", 6379)
v.SetDefault("event_broker.redis.password", "")
v.SetDefault("event_broker.redis.db", 0)
// Event Broker - NATS defaults
v.SetDefault("event_broker.nats.url", "nats://localhost:4222")
v.SetDefault("event_broker.nats.stream_name", "RESOLVESPEC_EVENTS")
v.SetDefault("event_broker.nats.subjects", []string{"events.>"})
v.SetDefault("event_broker.nats.storage", "file")
v.SetDefault("event_broker.nats.max_age", "24h")
// Event Broker - Database defaults
v.SetDefault("event_broker.database.table_name", "events")
v.SetDefault("event_broker.database.channel", "resolvespec_events")
v.SetDefault("event_broker.database.poll_interval", "1s")
// Event Broker - Retry Policy defaults
v.SetDefault("event_broker.retry_policy.max_retries", 3)
v.SetDefault("event_broker.retry_policy.initial_delay", "1s")
v.SetDefault("event_broker.retry_policy.max_delay", "30s")
v.SetDefault("event_broker.retry_policy.backoff_factor", 2.0)
// Paths defaults (common directory paths)
v.SetDefault("paths.data_dir", "./data")
v.SetDefault("paths.config_dir", "./config")
v.SetDefault("paths.logs_dir", "./logs")
v.SetDefault("paths.temp_dir", "./tmp")
// Extensions defaults (empty map)
v.SetDefault("extensions", map[string]interface{}{})
}
+117
View File
@@ -0,0 +1,117 @@
package config
import (
"fmt"
"os"
"path/filepath"
)
// Get retrieves a path by name
func (pc PathsConfig) Get(name string) (string, error) {
if pc == nil {
return "", fmt.Errorf("paths not initialized")
}
path, ok := pc[name]
if !ok {
return "", fmt.Errorf("path '%s' not found", name)
}
return path, nil
}
// GetOrDefault retrieves a path by name, returning defaultPath if not found
func (pc PathsConfig) GetOrDefault(name, defaultPath string) string {
if pc == nil {
return defaultPath
}
path, ok := pc[name]
if !ok {
return defaultPath
}
return path
}
// Set sets a path by name
func (pc PathsConfig) Set(name, path string) {
pc[name] = path
}
// Has checks if a path exists by name
func (pc PathsConfig) Has(name string) bool {
if pc == nil {
return false
}
_, ok := pc[name]
return ok
}
// EnsureDir ensures a directory exists at the specified path name
// Creates the directory if it doesn't exist with the given permissions
func (pc PathsConfig) EnsureDir(name string, perm os.FileMode) error {
path, err := pc.Get(name)
if err != nil {
return err
}
// Check if directory exists
info, err := os.Stat(path)
if err == nil {
// Path exists, check if it's a directory
if !info.IsDir() {
return fmt.Errorf("path '%s' exists but is not a directory: %s", name, path)
}
return nil
}
// Directory doesn't exist, create it
if os.IsNotExist(err) {
if err := os.MkdirAll(path, perm); err != nil {
return fmt.Errorf("failed to create directory for '%s' at %s: %w", name, path, err)
}
return nil
}
return fmt.Errorf("failed to stat path '%s' at %s: %w", name, path, err)
}
// AbsPath returns the absolute path for a named path
func (pc PathsConfig) AbsPath(name string) (string, error) {
path, err := pc.Get(name)
if err != nil {
return "", err
}
absPath, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("failed to get absolute path for '%s': %w", name, err)
}
return absPath, nil
}
// Join joins path segments with a named base path
func (pc PathsConfig) Join(name string, elem ...string) (string, error) {
base, err := pc.Get(name)
if err != nil {
return "", err
}
parts := append([]string{base}, elem...)
return filepath.Join(parts...), nil
}
// List returns all configured path names
func (pc PathsConfig) List() []string {
if pc == nil {
return []string{}
}
names := make([]string, 0, len(pc))
for name := range pc {
names = append(names, name)
}
return names
}
+149
View File
@@ -0,0 +1,149 @@
package config
import (
"fmt"
"net"
"os"
"strings"
)
// ApplyGlobalDefaults applies global server defaults to this instance
// Called for instances that don't specify their own timeout values
func (sic *ServerInstanceConfig) ApplyGlobalDefaults(globals ServersConfig) {
if sic.ShutdownTimeout == nil && globals.ShutdownTimeout > 0 {
t := globals.ShutdownTimeout
sic.ShutdownTimeout = &t
}
if sic.DrainTimeout == nil && globals.DrainTimeout > 0 {
t := globals.DrainTimeout
sic.DrainTimeout = &t
}
if sic.ReadTimeout == nil && globals.ReadTimeout > 0 {
t := globals.ReadTimeout
sic.ReadTimeout = &t
}
if sic.WriteTimeout == nil && globals.WriteTimeout > 0 {
t := globals.WriteTimeout
sic.WriteTimeout = &t
}
if sic.IdleTimeout == nil && globals.IdleTimeout > 0 {
t := globals.IdleTimeout
sic.IdleTimeout = &t
}
}
// Validate validates the ServerInstanceConfig
func (sic *ServerInstanceConfig) Validate() error {
if sic.Name == "" {
return fmt.Errorf("server instance name cannot be empty")
}
if sic.Port <= 0 || sic.Port > 65535 {
return fmt.Errorf("invalid port: %d (must be 1-65535)", sic.Port)
}
// Validate TLS options are mutually exclusive
tlsCount := 0
if sic.SSLCert != "" || sic.SSLKey != "" {
tlsCount++
}
if sic.SelfSignedSSL {
tlsCount++
}
if sic.AutoTLS {
tlsCount++
}
if tlsCount > 1 {
return fmt.Errorf("server '%s': only one TLS option can be enabled", sic.Name)
}
// If using certificate files, both must be provided
if (sic.SSLCert != "" && sic.SSLKey == "") || (sic.SSLCert == "" && sic.SSLKey != "") {
return fmt.Errorf("server '%s': both ssl_cert and ssl_key must be provided", sic.Name)
}
// If using AutoTLS, domains must be specified
if sic.AutoTLS && len(sic.AutoTLSDomains) == 0 {
return fmt.Errorf("server '%s': auto_tls_domains must be specified when auto_tls is enabled", sic.Name)
}
return nil
}
// Validate validates the ServersConfig
func (sc *ServersConfig) Validate() error {
if len(sc.Instances) == 0 {
return fmt.Errorf("at least one server instance must be configured")
}
if sc.DefaultServer != "" {
if _, ok := sc.Instances[sc.DefaultServer]; !ok {
return fmt.Errorf("default server '%s' not found in instances", sc.DefaultServer)
}
}
// Validate each instance
for name := range sc.Instances {
instance := sc.Instances[name]
if instance.Name != name {
return fmt.Errorf("server instance name mismatch: key='%s', name='%s'", name, instance.Name)
}
if err := instance.Validate(); err != nil {
return err
}
}
return nil
}
// GetDefault returns the default server instance configuration
func (sc *ServersConfig) GetDefault() (*ServerInstanceConfig, error) {
if sc.DefaultServer == "" {
return nil, fmt.Errorf("no default server configured")
}
instance, ok := sc.Instances[sc.DefaultServer]
if !ok {
return nil, fmt.Errorf("default server '%s' not found", sc.DefaultServer)
}
return &instance, nil
}
// GetIPs - GetIP for pc
func GetIPs() (hostname string, ipList string, ipNetList []net.IP) {
defer func() {
if err := recover(); err != nil {
fmt.Println("Recovered in GetIPs", err)
}
}()
hostname, _ = os.Hostname()
ipaddrlist := make([]net.IP, 0)
iplist := ""
addrs, err := net.LookupIP(hostname)
if err != nil {
return hostname, iplist, ipaddrlist
}
for _, a := range addrs {
// cfg.LogInfo("\nFound IP Host Address: %s", a)
if strings.Contains(a.String(), "127.0.0.1") {
continue
}
iplist = fmt.Sprintf("%s,%s", iplist, a)
ipaddrlist = append(ipaddrlist, a)
}
if iplist == "" {
iff, _ := net.InterfaceAddrs()
for _, a := range iff {
// cfg.LogInfo("\nFound IP Address: %s", a)
if strings.Contains(a.String(), "127.0.0.1") {
continue
}
iplist = fmt.Sprintf("%s,%s", iplist, a)
}
}
iplist = strings.TrimLeft(iplist, ",")
return hostname, iplist, ipaddrlist
}