torrust_tracker_deployer_lib/application/steps/infrastructure/plan.rs
1//! `OpenTofu` infrastructure planning step
2//!
3//! This module provides the `PlanInfrastructureStep` which handles `OpenTofu`
4//! planning by executing `tofu plan`. This step creates an execution plan
5//! showing what changes will be made to the infrastructure.
6//!
7//! ## Key Features
8//!
9//! - Infrastructure change planning and preview
10//! - Resource dependency analysis and ordering
11//! - Plan validation and error detection
12//! - Integration with `OpenTofuClient` for command execution
13//!
14//! ## Planning Process
15//!
16//! The step executes `tofu plan` which:
17//! - Analyzes current state vs desired configuration
18//! - Determines what resources need to be created, modified, or destroyed
19//! - Validates configuration and dependencies
20//! - Provides a preview of changes before application
21//!
22//! This step is crucial for validating infrastructure changes before applying them.
23
24use std::sync::Arc;
25
26use tracing::{info, instrument};
27
28use crate::adapters::tofu::client::OpenTofuClient;
29use crate::application::traits::CommandProgressListener;
30use crate::shared::command::CommandError;
31
32/// Simple step that plans `OpenTofu` configuration by executing `tofu plan`
33pub struct PlanInfrastructureStep {
34 opentofu_client: Arc<OpenTofuClient>,
35}
36
37impl PlanInfrastructureStep {
38 #[must_use]
39 pub fn new(opentofu_client: Arc<OpenTofuClient>) -> Self {
40 Self { opentofu_client }
41 }
42
43 /// Execute the `OpenTofu` plan step
44 ///
45 /// # Arguments
46 ///
47 /// * `listener` - Optional progress listener for reporting details
48 ///
49 /// # Errors
50 ///
51 /// Returns an error if:
52 /// * The `OpenTofu` plan fails
53 /// * The working directory does not exist or is not accessible
54 /// * The `OpenTofu` command execution fails
55 #[instrument(
56 name = "plan_infrastructure",
57 skip_all,
58 fields(step_type = "infrastructure", operation = "plan")
59 )]
60 pub fn execute(
61 &self,
62 listener: Option<&dyn CommandProgressListener>,
63 ) -> Result<(), CommandError> {
64 info!(
65 step = "plan_infrastructure",
66 "Planning OpenTofu infrastructure"
67 );
68
69 if let Some(l) = listener {
70 l.on_debug(&format!(
71 "Working directory: {}",
72 self.opentofu_client.working_dir().display()
73 ));
74 l.on_debug("Executing: tofu plan -var-file=variables.tfvars");
75 }
76
77 // Execute tofu plan command with variables file
78 let output = self.opentofu_client.plan(&["-var-file=variables.tfvars"])?;
79
80 // Extract and report plan summary if listener is provided
81 if let Some(l) = listener {
82 // Parse output to extract resource change counts
83 if let Some(plan_line) = output.lines().find(|line| line.contains("Plan:")) {
84 l.on_detail(plan_line.trim());
85 } else if output.contains("No changes") {
86 l.on_detail("Plan: No changes. Infrastructure is up-to-date.");
87 } else {
88 l.on_detail("Plan created successfully");
89 }
90 }
91
92 info!(
93 step = "plan_infrastructure",
94 status = "success",
95 "OpenTofu infrastructure planned successfully"
96 );
97
98 // Log output for debugging if needed
99 tracing::debug!(output = %output, "OpenTofu plan output");
100
101 Ok(())
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use std::sync::Arc;
108
109 use crate::adapters::tofu::client::OpenTofuClient;
110
111 use super::*;
112
113 #[test]
114 fn it_should_create_plan_infrastructure_step() {
115 let opentofu_client = Arc::new(OpenTofuClient::new("/tmp"));
116
117 let _step = PlanInfrastructureStep::new(opentofu_client);
118
119 // If we reach this point, the step was created successfully
120 }
121}