# Task 006: Environment Management
**Priority**: š Medium-High Impact
**Phase**: 2 (Ecosystem Integration)
**Estimated Effort**: 3-4 days
**Dependencies**: Task 003 (Config Validation), Task 005 (Serde Integration) recommended
## **Objective**
Implement comprehensive environment management capabilities to handle different deployment contexts (development, staging, production), making workspace_tools the standard choice for environment-aware applications.
## **Technical Requirements**
### **Core Features**
1. **Environment Detection**
- Automatic environment detection from various sources
- Environment variable priority system
- Default environment fallback
2. **Environment-Specific Configuration**
- Layered configuration loading by environment
- Environment variable overrides
- Secure secrets management per environment
3. **Environment Validation**
- Required environment variable checking
- Environment-specific validation rules
- Configuration completeness verification
### **New API Surface**
```rust
impl Workspace {
/// Get current environment (auto-detected)
pub fn current_environment(&self) -> Result<Environment>;
/// Load environment-specific configuration
pub fn load_env_config<T>(&self, config_name: &str) -> Result<T>
where
T: serde::de::DeserializeOwned;
/// Load configuration with explicit environment
pub fn load_config_for_env<T>(&self, config_name: &str, env: &Environment) -> Result<T>
where
T: serde::de::DeserializeOwned;
/// Validate environment setup
pub fn validate_environment(&self, env: &Environment) -> Result<EnvironmentValidation>;
/// Get environment-specific paths
pub fn env_config_dir(&self, env: &Environment) -> PathBuf;
pub fn env_data_dir(&self, env: &Environment) -> PathBuf;
pub fn env_cache_dir(&self, env: &Environment) -> PathBuf;
/// Check if environment variable exists and is valid
pub fn require_env_var(&self, key: &str) -> Result<String>;
pub fn get_env_var_or_default(&self, key: &str, default: &str) -> String;
}
#[derive(Debug, Clone, PartialEq)]
pub enum Environment {
Development,
Testing,
Staging,
Production,
Custom(String),
}
#[derive(Debug, Clone)]
pub struct EnvironmentValidation {
pub environment: Environment,
pub valid: bool,
pub missing_variables: Vec<String>,
pub invalid_variables: Vec<(String, String)>, // (key, reason)
pub warnings: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct EnvironmentConfig {
pub name: Environment,
pub required_vars: Vec<String>,
pub optional_vars: Vec<(String, String)>, // (key, default)
pub config_files: Vec<String>,
pub validation_rules: Vec<ValidationRule>,
}
#[derive(Debug, Clone)]
pub enum ValidationRule {
MinLength { var: String, min: usize },
Pattern { var: String, regex: String },
OneOf { var: String, values: Vec<String> },
FileExists { var: String },
UrlFormat { var: String },
}
```
### **Implementation Steps**
#### **Step 1: Environment Detection** (Day 1)
```rust
// Add to Cargo.toml
[features]
default = ["enabled", "environment"]
environment = [
"dep:regex",
"dep:once_cell",
]
[dependencies]
regex = { version = "1.0", optional = true }
once_cell = { version = "1.0", optional = true }
#[cfg(feature = "environment")]
mod environment {
use once_cell::sync::Lazy;
use std::env;
use crate::{WorkspaceError, Result};
static ENV_DETECTION_ORDER: Lazy<Vec<&'static str>> = Lazy::new(|| vec![
"WORKSPACE_ENV",
"APP_ENV",
"ENVIRONMENT",
"ENV",
"NODE_ENV", // For compatibility
"RAILS_ENV", // For compatibility
]);
impl Environment {
pub fn detect() -> Result<Environment> {
// Try environment variables in priority order
for env_var in ENV_DETECTION_ORDER.iter() {
if let Ok(value) = env::var(env_var) {
return Self::from_string(&value);
}
}
// Check for common development indicators
if Self::is_development_context()? {
return Ok(Environment::Development);
}
// Default to development if nothing found
Ok(Environment::Development)
}
fn from_string(s: &str) -> Result<Environment> {
match s.to_lowercase().as_str() {
"dev" | "development" | "local" => Ok(Environment::Development),
"test" | "testing" => Ok(Environment::Testing),
"stage" | "staging" => Ok(Environment::Staging),
"prod" | "production" => Ok(Environment::Production),
custom => Ok(Environment::Custom(custom.to_string())),
}
}
fn is_development_context() -> Result<bool> {
// Check for development indicators
Ok(
// Debug build
cfg!(debug_assertions) ||
// Cargo development mode
env::var("CARGO_PKG_NAME").is_ok() ||
// Common development paths
env::current_dir()
.map(|d| d.to_string_lossy().contains("src") ||
d.to_string_lossy().contains("dev"))
.unwrap_or(false)
)
}
pub fn as_str(&self) -> &str {
match self {
Environment::Development => "development",
Environment::Testing => "testing",
Environment::Staging => "staging",
Environment::Production => "production",
Environment::Custom(name) => name,
}
}
pub fn is_production(&self) -> bool {
matches!(self, Environment::Production)
}
pub fn is_development(&self) -> bool {
matches!(self, Environment::Development)
}
}
}
#[cfg(feature = "environment")]
impl Workspace {
pub fn current_environment(&self) -> Result<Environment> {
Environment::detect()
}
/// Get environment-specific configuration directory
pub fn env_config_dir(&self, env: &Environment) -> PathBuf {
self.config_dir().join(env.as_str())
}
/// Get environment-specific data directory
pub fn env_data_dir(&self, env: &Environment) -> PathBuf {
self.data_dir().join(env.as_str())
}
/// Get environment-specific cache directory
pub fn env_cache_dir(&self, env: &Environment) -> PathBuf {
self.cache_dir().join(env.as_str())
}
}
```
#### **Step 2: Environment-Specific Configuration Loading** (Day 2)
```rust
#[cfg(all(feature = "environment", feature = "serde_integration"))]
impl Workspace {
pub fn load_env_config<T>(&self, config_name: &str) -> Result<T>
where
T: serde::de::DeserializeOwned + ConfigMerge,
{
let env = self.current_environment()?;
self.load_config_for_env(config_name, &env)
}
pub fn load_config_for_env<T>(&self, config_name: &str, env: &Environment) -> Result<T>
where
T: serde::de::DeserializeOwned + ConfigMerge,
{
let config_layers = self.build_config_layers(config_name, env);
self.load_layered_config(&config_layers)
}
fn build_config_layers(&self, config_name: &str, env: &Environment) -> Vec<String> {
vec![
// Base configuration (always loaded first)
format!("{}.toml", config_name),
format!("{}.yaml", config_name),
format!("{}.json", config_name),
// Environment-specific configuration
format!("{}.{}.toml", config_name, env.as_str()),
format!("{}.{}.yaml", config_name, env.as_str()),
format!("{}.{}.json", config_name, env.as_str()),
// Local overrides (highest priority)
format!("{}.local.toml", config_name),
format!("{}.local.yaml", config_name),
format!("{}.local.json", config_name),
]
}
fn load_layered_config<T>(&self, config_files: &[String]) -> Result<T>
where
T: serde::de::DeserializeOwned + ConfigMerge,
{
let mut configs = Vec::new();
for config_file in config_files {
// Try different locations for each config file
let paths = vec![
self.config_dir().join(config_file),
self.env_config_dir(&self.current_environment()?).join(config_file),
self.join(config_file), // Root of workspace
];
for path in paths {
if path.exists() {
match self.load_config_from::<T>(&path) {
Ok(config) => {
configs.push(config);
break; // Found config, don't check other paths
}
Err(WorkspaceError::PathNotFound(_)) => continue,
Err(e) => return Err(e),
}
}
}
}
if configs.is_empty() {
return Err(WorkspaceError::PathNotFound(
self.config_dir().join(format!("no_config_found_for_{}",
config_files.first().unwrap_or(&"unknown".to_string()))
)
));
}
// Merge configurations (later configs override earlier ones)
let mut result = configs.into_iter().next().unwrap();
for config in configs {
result = result.merge(config);
}
Ok(result)
}
}
```
#### **Step 3: Environment Variable Management** (Day 2-3)
```rust
#[cfg(feature = "environment")]
impl Workspace {
pub fn require_env_var(&self, key: &str) -> Result<String> {
std::env::var(key).map_err(|_| {
WorkspaceError::ConfigurationError(
format!("Required environment variable '{}' not set", key)
)
})
}
pub fn get_env_var_or_default(&self, key: &str, default: &str) -> String {
std::env::var(key).unwrap_or_else(|_| default.to_string())
}
pub fn validate_environment(&self, env: &Environment) -> Result<EnvironmentValidation> {
let env_config = self.get_environment_config(env)?;
let mut validation = EnvironmentValidation {
environment: env.clone(),
valid: true,
missing_variables: Vec::new(),
invalid_variables: Vec::new(),
warnings: Vec::new(),
};
// Check required variables
for required_var in &env_config.required_vars {
if std::env::var(required_var).is_err() {
validation.missing_variables.push(required_var.clone());
validation.valid = false;
}
}
// Validate existing variables against rules
for rule in &env_config.validation_rules {
if let Err(error_msg) = self.validate_rule(rule) {
validation.invalid_variables.push((
self.rule_variable_name(rule).to_string(),
error_msg
));
validation.valid = false;
}
}
// Check for common misconfigurations
self.add_environment_warnings(env, &mut validation);
Ok(validation)
}
fn get_environment_config(&self, env: &Environment) -> Result<EnvironmentConfig> {
// Try to load environment config from file first
let env_config_path = self.config_dir().join(format!("environments/{}.toml", env.as_str()));
if env_config_path.exists() {
return self.load_config_from(&env_config_path);
}
// Return default configuration for known environments
Ok(match env {
Environment::Development => EnvironmentConfig {
name: env.clone(),
required_vars: vec!["DATABASE_URL".to_string()],
optional_vars: vec![
("LOG_LEVEL".to_string(), "debug".to_string()),
("PORT".to_string(), "8080".to_string()),
],
config_files: vec!["app.toml".to_string()],
validation_rules: vec![
ValidationRule::UrlFormat { var: "DATABASE_URL".to_string() },
],
},
Environment::Production => EnvironmentConfig {
name: env.clone(),
required_vars: vec![
"DATABASE_URL".to_string(),
"SECRET_KEY".to_string(),
"API_KEY".to_string(),
],
optional_vars: vec![
("LOG_LEVEL".to_string(), "info".to_string()),
("PORT".to_string(), "80".to_string()),
],
config_files: vec!["app.toml".to_string()],
validation_rules: vec![
ValidationRule::UrlFormat { var: "DATABASE_URL".to_string() },
ValidationRule::MinLength { var: "SECRET_KEY".to_string(), min: 32 },
ValidationRule::Pattern {
var: "API_KEY".to_string(),
regex: r"^[A-Za-z0-9_-]{32,}$".to_string()
},
],
},
_ => EnvironmentConfig {
name: env.clone(),
required_vars: vec![],
optional_vars: vec![],
config_files: vec!["app.toml".to_string()],
validation_rules: vec![],
},
})
}
fn validate_rule(&self, rule: &ValidationRule) -> Result<(), String> {
use regex::Regex;
match rule {
ValidationRule::MinLength { var, min } => {
let value = std::env::var(var).map_err(|_| format!("Variable '{}' not set", var))?;
if value.len() < *min {
return Err(format!("Must be at least {} characters", min));
}
}
ValidationRule::Pattern { var, regex } => {
let value = std::env::var(var).map_err(|_| format!("Variable '{}' not set", var))?;
let re = Regex::new(regex).map_err(|e| format!("Invalid regex: {}", e))?;
if !re.is_match(&value) {
return Err("Does not match required pattern".to_string());
}
}
ValidationRule::OneOf { var, values } => {
let value = std::env::var(var).map_err(|_| format!("Variable '{}' not set", var))?;
if !values.contains(&value) {
return Err(format!("Must be one of: {}", values.join(", ")));
}
}
ValidationRule::FileExists { var } => {
let path = std::env::var(var).map_err(|_| format!("Variable '{}' not set", var))?;
if !std::path::Path::new(&path).exists() {
return Err("File does not exist".to_string());
}
}
ValidationRule::UrlFormat { var } => {
let value = std::env::var(var).map_err(|_| format!("Variable '{}' not set", var))?;
// Simple URL validation
if !value.starts_with("http://") && !value.starts_with("https://") &&
!value.starts_with("postgres://") && !value.starts_with("mysql://") {
return Err("Must be a valid URL".to_string());
}
}
}
Ok(())
}
fn rule_variable_name(&self, rule: &ValidationRule) -> &str {
match rule {
ValidationRule::MinLength { var, .. } => var,
ValidationRule::Pattern { var, .. } => var,
ValidationRule::OneOf { var, .. } => var,
ValidationRule::FileExists { var } => var,
ValidationRule::UrlFormat { var } => var,
}
}
fn add_environment_warnings(&self, env: &Environment, validation: &mut EnvironmentValidation) {
match env {
Environment::Production => {
if std::env::var("DEBUG").unwrap_or_default() == "true" {
validation.warnings.push("DEBUG is enabled in production".to_string());
}
if std::env::var("LOG_LEVEL").unwrap_or_default() == "debug" {
validation.warnings.push("LOG_LEVEL set to debug in production".to_string());
}
}
Environment::Development => {
if std::env::var("SECRET_KEY").unwrap_or_default().len() < 16 {
validation.warnings.push("SECRET_KEY is short for development".to_string());
}
}
_ => {}
}
}
}
```
#### **Step 4: Environment Setup and Initialization** (Day 3-4)
```rust
#[cfg(feature = "environment")]
impl Workspace {
/// Initialize environment-specific directories and files
pub fn setup_environment(&self, env: &Environment) -> Result<()> {
// Create environment-specific directories
std::fs::create_dir_all(self.env_config_dir(env))
.map_err(|e| WorkspaceError::IoError(e.to_string()))?;
std::fs::create_dir_all(self.env_data_dir(env))
.map_err(|e| WorkspaceError::IoError(e.to_string()))?;
std::fs::create_dir_all(self.env_cache_dir(env))
.map_err(|e| WorkspaceError::IoError(e.to_string()))?;
// Create environment info file
let env_info = serde_json::json!({
"environment": env.as_str(),
"created_at": chrono::Utc::now().to_rfc3339(),
"workspace_root": self.root().to_string_lossy(),
});
let env_info_path = self.env_config_dir(env).join(".environment");
std::fs::write(&env_info_path, serde_json::to_string_pretty(&env_info)?)
.map_err(|e| WorkspaceError::IoError(e.to_string()))?;
Ok(())
}
/// Create environment template files
pub fn create_env_templates(&self, env: &Environment) -> Result<()> {
let env_config = self.get_environment_config(env)?;
// Create .env template file
let env_template = self.build_env_template(&env_config);
let env_template_path = self.env_config_dir(env).join(".env.template");
std::fs::write(&env_template_path, env_template)
.map_err(|e| WorkspaceError::IoError(e.to_string()))?;
// Create example configuration
let config_example = self.build_config_example(&env_config);
let config_example_path = self.env_config_dir(env).join("app.example.toml");
std::fs::write(&config_example_path, config_example)
.map_err(|e| WorkspaceError::IoError(e.to_string()))?;
Ok(())
}
fn build_env_template(&self, env_config: &EnvironmentConfig) -> String {
let mut template = format!("# Environment variables for {}\n\n", env_config.name.as_str());
template.push_str("# Required variables:\n");
for var in &env_config.required_vars {
template.push_str(&format!("{}=\n", var));
}
template.push_str("\n# Optional variables (with defaults):\n");
for (var, default) in &env_config.optional_vars {
template.push_str(&format!("{}={}\n", var, default));
}
template
}
fn build_config_example(&self, env_config: &EnvironmentConfig) -> String {
format!(r#"# Example configuration for {}
[app]
name = "my_application"
version = "0.1.0"
[server]
host = "127.0.0.1"
port = 8080
[database]
# Use environment variables for sensitive data
# url = "${{DATABASE_URL}}"
[logging]
level = "info"
format = "json"
# Environment: {}
"#, env_config.name.as_str(), env_config.name.as_str())
}
}
```
#### **Step 5: Testing and Integration** (Day 4)
```rust
#[cfg(test)]
#[cfg(feature = "environment")]
mod environment_tests {
use super::*;
use crate::testing::create_test_workspace_with_structure;
use std::env;
#[test]
fn test_environment_detection() {
// Test explicit environment variable
env::set_var("WORKSPACE_ENV", "production");
let env = Environment::detect().unwrap();
assert_eq!(env, Environment::Production);
env::set_var("WORKSPACE_ENV", "development");
let env = Environment::detect().unwrap();
assert_eq!(env, Environment::Development);
env::remove_var("WORKSPACE_ENV");
}
#[test]
fn test_environment_specific_paths() {
let (_temp_dir, ws) = create_test_workspace_with_structure();
let prod_env = Environment::Production;
let config_dir = ws.env_config_dir(&prod_env);
assert!(config_dir.to_string_lossy().contains("production"));
let data_dir = ws.env_data_dir(&prod_env);
assert!(data_dir.to_string_lossy().contains("production"));
}
#[test]
fn test_layered_config_loading() {
let (_temp_dir, ws) = create_test_workspace_with_structure();
#[derive(serde::Deserialize, Debug, PartialEq)]
struct TestConfig {
name: String,
port: u16,
debug: bool,
}
impl ConfigMerge for TestConfig {
fn merge(self, other: Self) -> Self {
Self {
name: other.name,
port: other.port,
debug: other.debug,
}
}
}
// Create base config
let base_config = r#"
name = "test_app"
port = 8080
debug = true
"#;
std::fs::write(ws.config_dir().join("app.toml"), base_config).unwrap();
// Create production override
let prod_config = r#"
port = 80
debug = false
"#;
std::fs::write(ws.config_dir().join("app.production.toml"), prod_config).unwrap();
// Load production config
let config: TestConfig = ws.load_config_for_env("app", &Environment::Production).unwrap();
assert_eq!(config.name, "test_app"); // From base
assert_eq!(config.port, 80); // From production override
assert_eq!(config.debug, false); // From production override
}
#[test]
fn test_environment_validation() {
let (_temp_dir, ws) = create_test_workspace_with_structure();
// Set up test environment variables
env::set_var("DATABASE_URL", "postgres://localhost/test");
env::set_var("SECRET_KEY", "test_secret_key_that_is_long_enough");
let validation = ws.validate_environment(&Environment::Development).unwrap();
assert!(validation.valid);
assert!(validation.missing_variables.is_empty());
// Test missing required variable
env::remove_var("DATABASE_URL");
let validation = ws.validate_environment(&Environment::Production).unwrap();
assert!(!validation.valid);
assert!(validation.missing_variables.contains(&"DATABASE_URL".to_string()));
// Cleanup
env::remove_var("SECRET_KEY");
}
#[test]
fn test_environment_setup() {
let (_temp_dir, ws) = create_test_workspace_with_structure();
let prod_env = Environment::Production;
ws.setup_environment(&prod_env).unwrap();
assert!(ws.env_config_dir(&prod_env).exists());
assert!(ws.env_data_dir(&prod_env).exists());
assert!(ws.env_cache_dir(&prod_env).exists());
assert!(ws.env_config_dir(&prod_env).join(".environment").exists());
}
#[test]
fn test_required_env_vars() {
let (_temp_dir, ws) = create_test_workspace_with_structure();
env::set_var("TEST_VAR", "test_value");
assert_eq!(ws.require_env_var("TEST_VAR").unwrap(), "test_value");
assert!(ws.require_env_var("NONEXISTENT_VAR").is_err());
assert_eq!(ws.get_env_var_or_default("NONEXISTENT_VAR", "default"), "default");
env::remove_var("TEST_VAR");
}
}
```
### **Documentation Updates**
#### **README.md Addition**
```markdown
## š environment management
workspace_tools provides comprehensive environment management for different deployment contexts:
```rust
use workspace_tools::{workspace, Environment};
let ws = workspace()?;
// Auto-detect current environment
let env = ws.current_environment()?;
// Load environment-specific configuration
let config: AppConfig = ws.load_env_config("app")?;
// Validate environment setup
let validation = ws.validate_environment(&env)?;
if !validation.valid {
println!("Missing variables: {:?}", validation.missing_variables);
}
```
**Features:**
- Automatic environment detection from multiple sources
- Layered configuration loading (base -> environment -> local)
- Environment variable validation and requirements
- Environment-specific directory structures
- Production safety checks and warnings
```
#### **New Example: environment_management.rs**
```rust
//! Environment management example
use workspace_tools::{workspace, Environment};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug)]
struct AppConfig {
name: String,
port: u16,
database_url: String,
debug: bool,
log_level: String,
}
impl workspace_tools::ConfigMerge for AppConfig {
fn merge(self, other: Self) -> Self {
Self {
name: other.name,
port: other.port,
database_url: other.database_url,
debug: other.debug,
log_level: other.log_level,
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let ws = workspace()?;
println!("š Environment Management Demo");
// Detect current environment
let current_env = ws.current_environment()?;
println!("Current environment: {:?}", current_env);
// Validate environment
let validation = ws.validate_environment(¤t_env)?;
if validation.valid {
println!("ā
Environment validation passed");
} else {
println!("ā Environment validation failed:");
for var in &validation.missing_variables {
println!(" Missing: {}", var);
}
for (var, reason) in &validation.invalid_variables {
println!(" Invalid {}: {}", var, reason);
}
}
// Show warnings
if !validation.warnings.is_empty() {
println!("ā ļø Warnings:");
for warning in &validation.warnings {
println!(" {}", warning);
}
}
// Load environment-specific configuration
match ws.load_env_config::<AppConfig>("app") {
Ok(config) => {
println!("š Configuration loaded:");
println!(" App: {} (port {})", config.name, config.port);
println!(" Database: {}", config.database_url);
println!(" Debug: {}", config.debug);
println!(" Log level: {}", config.log_level);
}
Err(e) => {
println!("ā Failed to load config: {}", e);
}
}
// Show environment-specific paths
println!("\nš Environment paths:");
println!(" Config: {}", ws.env_config_dir(¤t_env).display());
println!(" Data: {}", ws.env_data_dir(¤t_env).display());
println!(" Cache: {}", ws.env_cache_dir(¤t_env).display());
Ok(())
}
```
### **Success Criteria**
- [ ] Automatic environment detection from multiple sources
- [ ] Layered configuration loading (base -> env -> local)
- [ ] Environment variable validation and requirements
- [ ] Environment-specific directory management
- [ ] Production safety checks and warnings
- [ ] Support for custom environments
- [ ] Comprehensive test coverage
- [ ] Clear error messages for misconfigurations
### **Future Enhancements**
- Docker environment integration
- Kubernetes secrets and ConfigMap support
- Cloud provider environment detection (AWS, GCP, Azure)
- Environment migration tools
- Infrastructure as Code integration
- Environment diff and comparison tools
### **Breaking Changes**
None - this is purely additive functionality with feature flag.
This task makes workspace_tools the definitive solution for environment-aware Rust applications, handling the complexity of multi-environment deployments with ease.