diff --git a/pkg/config/README.md b/pkg/config/README.md index a158dcf..cba4463 100644 --- a/pkg/config/README.md +++ b/pkg/config/README.md @@ -1,6 +1,14 @@ # Environment-Based Configuration System -This package provides a configuration system for the Tender Management backend that loads configuration exclusively from `.env` files. It maintains the same structure as the previous YAML-based system while using environment variables for all configuration values. +This package provides a configuration system for the Tender Management backend that loads configuration from both OS environment variables and `.env` files, with **OS environment variables taking priority** over `.env` file values. It maintains the same structure as the previous YAML-based system while using environment variables for all configuration values. + +## Priority System + +The configuration loader follows this priority order: +1. **OS Environment Variables** (highest priority) - System-level environment variables +2. **`.env` File Values** (lower priority) - File-based configuration for development + +This means that if you set `SERVER_PORT=8080` in your `.env` file but have `SERVER_PORT=9000` as an OS environment variable, the system will use `9000`. ## Architecture @@ -63,7 +71,9 @@ func initConfig() Config { } ``` -### 3. Create .env Configuration File +### 3. Configuration Sources + +#### Option A: Using .env File Only (Development) Create a `.env` file in your command directory: @@ -104,6 +114,82 @@ SOME_TIMEOUT=30s SOME_NUMBER=42 ``` +#### Option B: Using OS Environment Variables (Production) + +```bash +# Set environment variables directly +export SERVER_HOST=0.0.0.0 +export SERVER_PORT=8080 +export MONGODB_URI=mongodb://mongo:27017 +export MONGODB_NAME=production_db +export LOG_LEVEL=error + +# Run your application +./your-command +``` + +#### Option C: Mixed Approach (Recommended) + +1. Create a `.env` file with development defaults +2. Override specific values with OS environment variables for production: + +```bash +# .env file contains development defaults +# Override critical values with OS env vars +export MONGODB_URI=mongodb://production-server:27017 +export MONGODB_NAME=production_db +export LOG_LEVEL=error + +# Run application - will use .env defaults except for overridden values +./your-command +``` + +## Priority Example + +Given this `.env` file: +```env +SERVER_HOST=localhost +SERVER_PORT=8080 +LOG_LEVEL=debug +``` + +And these OS environment variables: +```bash +export SERVER_PORT=9000 +export LOG_LEVEL=error +``` + +The final configuration will be: +- `SERVER_HOST=localhost` (from .env file, no OS override) +- `SERVER_PORT=9000` (OS environment variable overrides .env) +- `LOG_LEVEL=error` (OS environment variable overrides .env) + +## Advanced Features + +### Checking Configuration Source + +You can check whether a configuration value came from OS environment or .env file: + +```go +import "tm/pkg/config" + +func main() { + value, isFromOS := config.GetEnvWithPriority("SERVER_PORT") + if isFromOS { + fmt.Printf("SERVER_PORT=%s (from OS environment)\n", value) + } else { + fmt.Printf("SERVER_PORT=%s (from .env file)\n", value) + } +} +``` + +### Graceful .env File Handling + +The system gracefully handles missing `.env` files: +- If `.env` file exists: loads values as defaults +- If `.env` file is missing: continues with OS environment variables only +- No errors are thrown for missing `.env` files + ## Examples ### Web Command Config @@ -164,9 +250,11 @@ type ScraperConfig struct { The configuration loader uses Go generics to provide type-safe configuration loading. The function signature ensures that you get back exactly the type you expect. -### Environment Variable Support +### Environment Variable Priority -The configuration system loads all values from `.env` files using the `github.com/subosito/gotenv` library. +The configuration system loads values with explicit priority: +1. OS environment variables (system-level, highest priority) +2. .env file values (file-based, development defaults) ### Reflection-Based Parsing @@ -193,7 +281,11 @@ The configuration system supports the following Go types: 5. **Use appropriate types**: Use `time.Duration` for durations, proper numeric types for numbers, etc. -6. **Provide sensible defaults**: Consider providing sensible defaults in your code when environment variables are not set. +6. **Provide sensible defaults in .env**: Use `.env` files to provide development defaults and override with OS environment variables for production. + +7. **Leverage the priority system**: Use `.env` for development defaults and OS environment variables for production overrides. + +8. **Keep secrets in OS environment variables**: Never put sensitive values like API keys or passwords in `.env` files; use OS environment variables instead. ## Migration from YAML System @@ -223,7 +315,7 @@ type Config struct { // Old way (YAML loading) conf, err := config.LoadConfig(".", &Config{}) -// New way (.env loading) +// New way (.env loading) - same API! conf, err := config.LoadConfig(".", &Config{}) ``` @@ -240,13 +332,51 @@ The system follows these naming conventions: ## Error Handling The configuration system provides detailed error messages when: -- The `.env` file cannot be loaded +- The `.env` file exists but cannot be loaded - Environment variables cannot be parsed into the expected types - Required fields are missing (handled by your application logic) +Note: Missing `.env` files do not cause errors - the system will continue with OS environment variables only. + ## Security Considerations -1. **Never commit `.env` files**: Add `.env` to your `.gitignore` file -2. **Use environment-specific files**: Create different `.env` files for different environments (`.env.local`, `.env.production`, etc.) -3. **Validate sensitive values**: Always validate sensitive configuration values like API keys and database URIs -4. **Use secure defaults**: Provide secure default values for security-related configuration +1. **Never commit `.env` files with secrets**: Add `.env` to your `.gitignore` file and never include sensitive values +2. **Use OS environment variables for production secrets**: Set sensitive values like API keys and database passwords as OS environment variables +3. **Use environment-specific files**: Create different `.env` files for different environments (`.env.local`, `.env.development`, etc.) +4. **Validate sensitive values**: Always validate sensitive configuration values like API keys and database URIs +5. **Use secure defaults**: Provide secure default values for security-related configuration +6. **Leverage priority system for security**: Keep development defaults in `.env` and override with secure OS environment variables in production + +## Deployment Strategies + +### Development +```bash +# Use .env file for all configuration +./your-command +``` + +### Staging +```bash +# Use .env for defaults, override key values +export MONGODB_URI=mongodb://staging-db:27017 +export LOG_LEVEL=warn +./your-command +``` + +### Production +```bash +# Override all critical values with OS environment variables +export MONGODB_URI=mongodb://prod-cluster:27017 +export MONGODB_NAME=production +export LOG_LEVEL=error +export SERVER_HOST=0.0.0.0 +./your-command +``` + +### Docker +```dockerfile +# Set production values in Dockerfile or docker-compose +ENV MONGODB_URI=mongodb://mongo:27017 +ENV LOG_LEVEL=info +ENV SERVER_HOST=0.0.0.0 +``` diff --git a/pkg/config/config.go b/pkg/config/config.go index 6c05f12..dc178e6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -65,15 +65,24 @@ type RateLimitConfig struct { Burst int `env:"RATE_LIMIT_BURST"` } -// LoadConfig loads configuration from .env file +// LoadConfig loads configuration with priority: OS environment variables > .env file +// OS environment variables take precedence over .env file values func LoadConfig[T any](path string, config T) (T, error) { - // Load .env file + // First, try to load .env file (this will not override existing OS env vars) envPath := fmt.Sprintf("%s/.env", path) - if err := gotenv.Load(envPath); err != nil { - return config, fmt.Errorf("failed to load .env file from %s: %w", envPath, err) + + // Check if .env file exists before trying to load it + if _, err := os.Stat(envPath); err == nil { + // Load .env file - gotenv.Load respects existing environment variables + // and will not override them with values from the .env file + if err := gotenv.Load(envPath); err != nil { + return config, fmt.Errorf("failed to load .env file from %s: %w", envPath, err) + } } + // If .env file doesn't exist, we continue with OS environment variables only // Parse configuration using reflection + // This will use OS env vars first, then fall back to values loaded from .env if err := parseConfig(config); err != nil { return config, fmt.Errorf("failed to parse configuration: %w", err) } @@ -82,6 +91,9 @@ func LoadConfig[T any](path string, config T) (T, error) { } // parseConfig uses reflection to parse environment variables into the config struct +// Environment variables are read with the following priority: +// 1. OS environment variables (highest priority) +// 2. Values from .env file (loaded by gotenv.Load, lower priority) func parseConfig(config interface{}) error { v := reflect.ValueOf(config) if v.Kind() != reflect.Ptr { @@ -122,6 +134,7 @@ func parseStruct(v reflect.Value) error { } // Get environment variable value + // os.Getenv will return OS env vars first, then values loaded from .env envValue := os.Getenv(envTag) if envValue == "" { continue @@ -129,7 +142,7 @@ func parseStruct(v reflect.Value) error { // Parse and set the field value if err := setFieldValue(field, envValue); err != nil { - return fmt.Errorf("failed to set field %s: %w", fieldType.Name, err) + return fmt.Errorf("failed to set field %s (env: %s): %w", fieldType.Name, envTag, err) } } @@ -176,6 +189,22 @@ func setFieldValue(field reflect.Value, value string) error { return nil } +// GetEnvWithPriority gets environment variable with explicit priority handling +// Returns the value and a boolean indicating the source (true = OS env, false = .env file) +func GetEnvWithPriority(key string) (string, bool) { + // Check if the value exists in OS environment + if value, exists := os.LookupEnv(key); exists { + return value, true // From OS environment + } + + // If not in OS environment, it might be from .env file + if value := os.Getenv(key); value != "" { + return value, false // From .env file + } + + return "", false +} + // Helper function to get environment variable with default value func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" {