Skip to main content
Glama
state_analyzer.rs15.4 kB
//! State analyzer for Terraform state analysis with drift detection. use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// Resource statistics grouped by provider #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ProviderStats { pub name: String, pub resource_count: i32, pub resource_types: Vec<String>, } /// Resource statistics grouped by type #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TypeStats { pub resource_type: String, pub count: i32, pub addresses: Vec<String>, } /// Drift detection result for a single resource #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DriftResult { pub address: String, pub resource_type: String, pub drift_type: DriftType, pub details: Option<String>, } /// Type of drift detected #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum DriftType { /// Resource exists in state but may be modified in cloud Modified, /// Resource exists in state but not in cloud (deleted externally) Deleted, /// Resource exists in cloud but not in state (created externally) Orphaned, /// Resource configuration differs from state ConfigurationDrift, } /// Health check result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HealthCheck { pub name: String, pub status: HealthStatus, pub message: String, } /// Health status #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum HealthStatus { Healthy, Warning, Critical, } /// Resource in state #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StateResource { pub address: String, pub resource_type: String, pub provider: String, pub module: Option<String>, pub tainted: bool, pub attributes: Option<serde_json::Value>, } /// Complete state analysis result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StateAnalysis { pub total_resources: i32, pub providers: Vec<ProviderStats>, pub types: Vec<TypeStats>, pub resources: Vec<StateResource>, pub drift_results: Vec<DriftResult>, pub health_checks: Vec<HealthCheck>, pub state_version: Option<i32>, pub terraform_version: Option<String>, pub serial: Option<i64>, } /// Terraform state JSON structure #[derive(Debug, Deserialize)] struct TerraformStateJson { version: Option<i32>, terraform_version: Option<String>, serial: Option<i64>, resources: Option<Vec<StateResourceJson>>, } #[derive(Debug, Deserialize)] struct StateResourceJson { #[serde(rename = "type")] resource_type: String, name: String, provider: String, module: Option<String>, instances: Option<Vec<StateInstance>>, } #[derive(Debug, Deserialize)] struct StateInstance { attributes: Option<serde_json::Value>, #[serde(default)] status: Option<String>, index_key: Option<serde_json::Value>, } /// Analyze terraform state pub fn analyze_state( state_json: &str, resource_type_filter: Option<&str>, detect_drift: bool, ) -> anyhow::Result<StateAnalysis> { let state: TerraformStateJson = serde_json::from_str(state_json)?; let mut resources = Vec::new(); let mut provider_map: HashMap<String, ProviderStats> = HashMap::new(); let mut type_map: HashMap<String, TypeStats> = HashMap::new(); if let Some(state_resources) = state.resources { for resource in state_resources { // Apply type filter if specified if let Some(filter) = resource_type_filter { if !resource.resource_type.contains(filter) { continue; } } let provider_name = extract_provider_name(&resource.provider); // Process each instance of the resource if let Some(instances) = resource.instances { for (idx, instance) in instances.iter().enumerate() { let address = if instances.len() == 1 { format_address(&resource.module, &resource.resource_type, &resource.name) } else if let Some(ref key) = instance.index_key { format!( "{}[{}]", format_address( &resource.module, &resource.resource_type, &resource.name ), key ) } else { format!( "{}[{}]", format_address( &resource.module, &resource.resource_type, &resource.name ), idx ) }; let tainted = instance.status.as_ref().is_some_and(|s| s == "tainted"); resources.push(StateResource { address: address.clone(), resource_type: resource.resource_type.clone(), provider: provider_name.clone(), module: resource.module.clone(), tainted, attributes: instance.attributes.clone(), }); // Update provider stats let provider_stats = provider_map .entry(provider_name.clone()) .or_insert_with(|| ProviderStats { name: provider_name.clone(), resource_count: 0, resource_types: Vec::new(), }); provider_stats.resource_count += 1; if !provider_stats .resource_types .contains(&resource.resource_type) { provider_stats .resource_types .push(resource.resource_type.clone()); } // Update type stats let type_stats = type_map .entry(resource.resource_type.clone()) .or_insert_with(|| TypeStats { resource_type: resource.resource_type.clone(), count: 0, addresses: Vec::new(), }); type_stats.count += 1; type_stats.addresses.push(address); } } } } let drift_results = if detect_drift { detect_drift_issues(&resources) } else { Vec::new() }; let health_checks = run_health_checks(&resources, &provider_map); let mut providers: Vec<ProviderStats> = provider_map.into_values().collect(); providers.sort_by(|a, b| b.resource_count.cmp(&a.resource_count)); let mut types: Vec<TypeStats> = type_map.into_values().collect(); types.sort_by(|a, b| b.count.cmp(&a.count)); Ok(StateAnalysis { total_resources: resources.len() as i32, providers, types, resources, drift_results, health_checks, state_version: state.version, terraform_version: state.terraform_version, serial: state.serial, }) } /// Format resource address fn format_address(module: &Option<String>, resource_type: &str, name: &str) -> String { match module { Some(m) if !m.is_empty() => format!("{}.{}.{}", m, resource_type, name), _ => format!("{}.{}", resource_type, name), } } /// Extract provider name from provider string (e.g., "provider[\"registry.terraform.io/hashicorp/aws\"]") fn extract_provider_name(provider: &str) -> String { if provider.contains('/') { // Extract from full provider path provider .rsplit('/') .next() .unwrap_or(provider) .trim_end_matches(']') .trim_end_matches('"') .to_string() } else { provider.to_string() } } /// Detect potential drift issues (heuristic-based) fn detect_drift_issues(resources: &[StateResource]) -> Vec<DriftResult> { let mut drift_results = Vec::new(); for resource in resources { // Check for tainted resources if resource.tainted { drift_results.push(DriftResult { address: resource.address.clone(), resource_type: resource.resource_type.clone(), drift_type: DriftType::Modified, details: Some("Resource is marked as tainted".to_string()), }); } // Check for resources without attributes (may indicate issues) if resource.attributes.is_none() { drift_results.push(DriftResult { address: resource.address.clone(), resource_type: resource.resource_type.clone(), drift_type: DriftType::ConfigurationDrift, details: Some("Resource has no attributes in state".to_string()), }); } // Check for data sources that might have stale data if resource.resource_type.starts_with("data.") { drift_results.push(DriftResult { address: resource.address.clone(), resource_type: resource.resource_type.clone(), drift_type: DriftType::ConfigurationDrift, details: Some( "Data source may have stale data - run refresh to update".to_string(), ), }); } } drift_results } /// Run health checks on the state fn run_health_checks( resources: &[StateResource], providers: &HashMap<String, ProviderStats>, ) -> Vec<HealthCheck> { let mut checks = Vec::new(); // Check for empty state if resources.is_empty() { checks.push(HealthCheck { name: "state_not_empty".to_string(), status: HealthStatus::Warning, message: "State is empty - no resources are being managed".to_string(), }); } else { checks.push(HealthCheck { name: "state_not_empty".to_string(), status: HealthStatus::Healthy, message: format!("State contains {} resources", resources.len()), }); } // Check for tainted resources let tainted_count = resources.iter().filter(|r| r.tainted).count(); if tainted_count > 0 { checks.push(HealthCheck { name: "no_tainted_resources".to_string(), status: HealthStatus::Warning, message: format!( "{} tainted resources found - they will be recreated on next apply", tainted_count ), }); } else { checks.push(HealthCheck { name: "no_tainted_resources".to_string(), status: HealthStatus::Healthy, message: "No tainted resources found".to_string(), }); } // Check for resources in modules let module_resources = resources.iter().filter(|r| r.module.is_some()).count(); if module_resources > 0 && resources.len() > 10 { let percentage = (module_resources as f64 / resources.len() as f64) * 100.0; if percentage < 50.0 { checks.push(HealthCheck { name: "module_usage".to_string(), status: HealthStatus::Warning, message: format!( "Only {:.0}% of resources are in modules - consider modularizing", percentage ), }); } else { checks.push(HealthCheck { name: "module_usage".to_string(), status: HealthStatus::Healthy, message: format!("{:.0}% of resources are organized in modules", percentage), }); } } // Check for provider diversity (potential complexity) if providers.len() > 5 { checks.push(HealthCheck { name: "provider_count".to_string(), status: HealthStatus::Warning, message: format!( "{} providers in use - consider if all are necessary", providers.len() ), }); } else { checks.push(HealthCheck { name: "provider_count".to_string(), status: HealthStatus::Healthy, message: format!("{} providers in use", providers.len()), }); } checks } #[cfg(test)] mod tests { use super::*; #[test] fn test_extract_provider_name() { assert_eq!( extract_provider_name("provider[\"registry.terraform.io/hashicorp/aws\"]"), "aws" ); assert_eq!(extract_provider_name("aws"), "aws"); } #[test] fn test_format_address() { assert_eq!( format_address(&None, "aws_instance", "example"), "aws_instance.example" ); assert_eq!( format_address(&Some("module.vpc".to_string()), "aws_subnet", "public"), "module.vpc.aws_subnet.public" ); } #[test] fn test_analyze_empty_state() { let state_json = r#"{"version": 4, "terraform_version": "1.5.0", "resources": []}"#; let result = analyze_state(state_json, None, false).unwrap(); assert_eq!(result.total_resources, 0); assert_eq!(result.state_version, Some(4)); } #[test] fn test_analyze_state_with_resources() { let state_json = r#"{ "version": 4, "terraform_version": "1.5.0", "serial": 123, "resources": [ { "type": "aws_instance", "name": "example", "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", "instances": [ {"attributes": {"id": "i-12345"}} ] } ] }"#; let result = analyze_state(state_json, None, false).unwrap(); assert_eq!(result.total_resources, 1); assert_eq!(result.resources[0].address, "aws_instance.example"); assert_eq!(result.resources[0].provider, "aws"); } #[test] fn test_type_filter() { let state_json = r#"{ "version": 4, "resources": [ { "type": "aws_instance", "name": "web", "provider": "aws", "instances": [{"attributes": {}}] }, { "type": "aws_s3_bucket", "name": "storage", "provider": "aws", "instances": [{"attributes": {}}] } ] }"#; let result = analyze_state(state_json, Some("s3"), false).unwrap(); assert_eq!(result.total_resources, 1); assert!(result.resources[0].resource_type.contains("s3")); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nwiizo/tfmcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server