Skip to main content
Glama
refresh.rs7.72 kB
//! Terraform refresh operations. //! //! Note: `terraform refresh` is deprecated. The recommended approach is to use //! `terraform apply -refresh-only` which is what this module implements internally. use serde::{Deserialize, Serialize}; use std::path::Path; use std::process::Command; /// Result of refresh operation #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RefreshResult { pub success: bool, pub resources_updated: i32, pub output: String, pub changes: Vec<RefreshChange>, pub message: String, } /// A single refresh change #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RefreshChange { pub resource_address: String, pub change_type: RefreshChangeType, pub detail: Option<String>, } /// Type of refresh change #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum RefreshChangeType { Updated, Drifted, Unchanged, } /// Execute terraform refresh (via apply -refresh-only) pub fn execute_refresh( terraform_path: &Path, project_dir: &Path, target: Option<&str>, ) -> anyhow::Result<RefreshResult> { let mut cmd = Command::new(terraform_path); cmd.arg("apply") .arg("-refresh-only") .arg("-auto-approve") .arg("-json"); // If targeting a specific resource if let Some(target_addr) = target { cmd.arg(format!("-target={}", target_addr)); } let output = cmd.current_dir(project_dir).output()?; let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); if !output.status.success() { return Err(anyhow::anyhow!("Refresh failed: {}", stderr)); } // Parse JSON output to extract changes let changes = parse_refresh_output(&stdout); let resources_updated = changes .iter() .filter(|c| c.change_type == RefreshChangeType::Updated) .count() as i32; let message = if resources_updated > 0 { format!("Refreshed {} resources", resources_updated) } else { "No resources needed refreshing".to_string() }; Ok(RefreshResult { success: true, resources_updated, output: stdout.to_string(), changes, message, }) } /// Execute refresh with plan preview (no auto-approve) #[allow(dead_code)] pub fn preview_refresh( terraform_path: &Path, project_dir: &Path, target: Option<&str>, ) -> anyhow::Result<RefreshResult> { let mut cmd = Command::new(terraform_path); cmd.arg("plan").arg("-refresh-only").arg("-json"); // If targeting a specific resource if let Some(target_addr) = target { cmd.arg(format!("-target={}", target_addr)); } let output = cmd.current_dir(project_dir).output()?; let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); if !output.status.success() { return Err(anyhow::anyhow!("Refresh preview failed: {}", stderr)); } // Parse JSON output to extract changes let changes = parse_refresh_output(&stdout); let resources_updated = changes .iter() .filter(|c| c.change_type == RefreshChangeType::Drifted) .count() as i32; let message = if resources_updated > 0 { format!( "{} resources have drifted and would be updated", resources_updated ) } else { "No drift detected - state is up to date".to_string() }; Ok(RefreshResult { success: true, resources_updated, output: stdout.to_string(), changes, message, }) } /// Parse refresh output to extract changes fn parse_refresh_output(json_output: &str) -> Vec<RefreshChange> { let mut changes = Vec::new(); for line in json_output.lines() { if line.trim().is_empty() { continue; } if let Ok(obj) = serde_json::from_str::<serde_json::Value>(line) { // Check for resource_drift or planned_change messages if let Some(msg_type) = obj.get("type").and_then(|t| t.as_str()) { match msg_type { "resource_drift" => { if let Some(change) = obj.get("change") { if let Some(resource) = change.get("resource") { if let Some(addr) = resource.get("addr").and_then(|a| a.as_str()) { changes.push(RefreshChange { resource_address: addr.to_string(), change_type: RefreshChangeType::Drifted, detail: Some("Resource has drifted from state".to_string()), }); } } } } "planned_change" => { if let Some(change) = obj.get("change") { if let Some(resource) = change.get("resource") { if let Some(addr) = resource.get("addr").and_then(|a| a.as_str()) { let action = change .get("action") .and_then(|a| a.as_str()) .unwrap_or("update"); if action == "update" { changes.push(RefreshChange { resource_address: addr.to_string(), change_type: RefreshChangeType::Updated, detail: Some("State will be updated".to_string()), }); } } } } } "apply_complete" | "change_summary" => { // Skip summary messages } _ => {} } } } } changes } /// Get a list of resources that might need refreshing #[allow(dead_code)] pub fn get_stale_resources( terraform_path: &Path, project_dir: &Path, ) -> anyhow::Result<Vec<String>> { // Run a refresh-only plan to detect drift let result = preview_refresh(terraform_path, project_dir, None)?; let stale: Vec<String> = result .changes .into_iter() .filter(|c| c.change_type == RefreshChangeType::Drifted) .map(|c| c.resource_address) .collect(); Ok(stale) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_empty_output() { let changes = parse_refresh_output(""); assert!(changes.is_empty()); } #[test] fn test_parse_drift_message() { let json = r#"{"type":"resource_drift","change":{"resource":{"addr":"aws_instance.example"}}}"#; let changes = parse_refresh_output(json); assert_eq!(changes.len(), 1); assert_eq!(changes[0].resource_address, "aws_instance.example"); assert_eq!(changes[0].change_type, RefreshChangeType::Drifted); } #[test] fn test_parse_planned_change() { let json = r#"{"type":"planned_change","change":{"resource":{"addr":"aws_s3_bucket.data"},"action":"update"}}"#; let changes = parse_refresh_output(json); assert_eq!(changes.len(), 1); assert_eq!(changes[0].change_type, RefreshChangeType::Updated); } }

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