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:
+141
-11
@@ -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
@@ -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 != "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user