Enhance Configuration System with Environment Variable Priority Handling

- Updated the configuration loading mechanism to prioritize OS environment variables over .env file values, improving flexibility and security.
- Added a new function, GetEnvWithPriority, to explicitly check the source of configuration values.
- Revised documentation to reflect the new priority system, including detailed examples and usage instructions for mixed configuration approaches.
- Enhanced error handling for .env file loading, ensuring graceful handling of missing files without disrupting the application flow.
This commit is contained in:
n.nakhostin
2025-09-15 14:52:12 +03:30
parent c701053609
commit 05a307e345
2 changed files with 175 additions and 16 deletions
+141 -11
View File
@@ -1,6 +1,14 @@
# Environment-Based Configuration System # 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 ## 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: Create a `.env` file in your command directory:
@@ -104,6 +114,82 @@ SOME_TIMEOUT=30s
SOME_NUMBER=42 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 ## Examples
### Web Command Config ### 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. 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 ### 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. 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 ## Migration from YAML System
@@ -223,7 +315,7 @@ type Config struct {
// Old way (YAML loading) // Old way (YAML loading)
conf, err := config.LoadConfig(".", &Config{}) conf, err := config.LoadConfig(".", &Config{})
// New way (.env loading) // New way (.env loading) - same API!
conf, err := config.LoadConfig(".", &Config{}) conf, err := config.LoadConfig(".", &Config{})
``` ```
@@ -240,13 +332,51 @@ The system follows these naming conventions:
## Error Handling ## Error Handling
The configuration system provides detailed error messages when: 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 - Environment variables cannot be parsed into the expected types
- Required fields are missing (handled by your application logic) - 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 ## Security Considerations
1. **Never commit `.env` files**: Add `.env` to your `.gitignore` file 1. **Never commit `.env` files with secrets**: Add `.env` to your `.gitignore` file and never include sensitive values
2. **Use environment-specific files**: Create different `.env` files for different environments (`.env.local`, `.env.production`, etc.) 2. **Use OS environment variables for production secrets**: Set sensitive values like API keys and database passwords as OS environment variables
3. **Validate sensitive values**: Always validate sensitive configuration values like API keys and database URIs 3. **Use environment-specific files**: Create different `.env` files for different environments (`.env.local`, `.env.development`, etc.)
4. **Use secure defaults**: Provide secure default values for security-related configuration 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
```
+34 -5
View File
@@ -65,15 +65,24 @@ type RateLimitConfig struct {
Burst int `env:"RATE_LIMIT_BURST"` 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) { 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) 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 // 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 { if err := parseConfig(config); err != nil {
return config, fmt.Errorf("failed to parse configuration: %w", err) 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 // 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 { func parseConfig(config interface{}) error {
v := reflect.ValueOf(config) v := reflect.ValueOf(config)
if v.Kind() != reflect.Ptr { if v.Kind() != reflect.Ptr {
@@ -122,6 +134,7 @@ func parseStruct(v reflect.Value) error {
} }
// Get environment variable value // Get environment variable value
// os.Getenv will return OS env vars first, then values loaded from .env
envValue := os.Getenv(envTag) envValue := os.Getenv(envTag)
if envValue == "" { if envValue == "" {
continue continue
@@ -129,7 +142,7 @@ func parseStruct(v reflect.Value) error {
// Parse and set the field value // Parse and set the field value
if err := setFieldValue(field, envValue); err != nil { 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 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 // Helper function to get environment variable with default value
func getEnv(key, defaultValue string) string { func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" { if value := os.Getenv(key); value != "" {