Skip to main content

torrust_tracker_deployer_lib/application/steps/infrastructure/
apply.rs

1//! `OpenTofu` infrastructure application step
2//!
3//! This module provides the `ApplyInfrastructureStep` which handles `OpenTofu`
4//! application by executing `tofu apply`. This step applies the planned
5//! infrastructure changes to provision or modify resources.
6//!
7//! ## Key Features
8//!
9//! - Infrastructure provisioning and resource creation
10//! - Configurable auto-approval for automation scenarios
11//! - Progress tracking and status reporting
12//! - Integration with `OpenTofuClient` for command execution
13//!
14//! ## Application Process
15//!
16//! The step executes `tofu apply` which:
17//! - Applies planned changes to create/modify/destroy resources
18//! - Manages resource dependencies and ordering
19//! - Updates infrastructure state with actual resource information
20//! - Provides detailed progress and completion status
21//!
22//! This step is where actual infrastructure provisioning occurs.
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 applies `OpenTofu` configuration by executing `tofu apply`
33pub struct ApplyInfrastructureStep {
34    opentofu_client: Arc<OpenTofuClient>,
35    auto_approve: bool,
36}
37
38impl ApplyInfrastructureStep {
39    #[must_use]
40    pub fn new(opentofu_client: Arc<OpenTofuClient>) -> Self {
41        Self {
42            opentofu_client,
43            auto_approve: true, // Default to auto-approve for automation
44        }
45    }
46
47    #[must_use]
48    pub fn with_auto_approve(mut self, auto_approve: bool) -> Self {
49        self.auto_approve = auto_approve;
50        self
51    }
52
53    /// Execute the `OpenTofu` apply step
54    ///
55    /// # Arguments
56    ///
57    /// * `listener` - Optional progress listener for reporting details
58    ///
59    /// # Errors
60    ///
61    /// Returns an error if:
62    /// * The `OpenTofu` apply fails
63    /// * The working directory does not exist or is not accessible
64    /// * The `OpenTofu` command execution fails
65    #[instrument(
66        name = "apply_infrastructure",
67        skip_all,
68        fields(
69            step_type = "infrastructure",
70            operation = "apply",
71            auto_approve = %self.auto_approve
72        )
73    )]
74    pub fn execute(
75        &self,
76        listener: Option<&dyn CommandProgressListener>,
77    ) -> Result<(), CommandError> {
78        info!(
79            step = "apply_infrastructure",
80            auto_approve = self.auto_approve,
81            "Applying OpenTofu infrastructure"
82        );
83
84        if let Some(l) = listener {
85            l.on_debug(&format!(
86                "Working directory: {}",
87                self.opentofu_client.working_dir().display()
88            ));
89            l.on_debug(&format!(
90                "Executing: tofu apply -var-file=variables.tfvars{}",
91                if self.auto_approve {
92                    " -auto-approve"
93                } else {
94                    ""
95                }
96            ));
97        }
98
99        // Execute tofu apply command with variables file
100        let output = self
101            .opentofu_client
102            .apply(self.auto_approve, &["-var-file=variables.tfvars"])?;
103
104        // Report apply completion details if listener is provided
105        if let Some(l) = listener {
106            // Check for resource creation/modification in output
107            if output.contains("Creation complete") || output.contains("Modifications complete") {
108                l.on_detail("Infrastructure resources created successfully");
109            } else if output.contains("Apply complete") {
110                l.on_detail("Infrastructure applied successfully");
111            }
112        }
113
114        info!(
115            step = "apply_infrastructure",
116            status = "success",
117            "OpenTofu infrastructure applied successfully"
118        );
119
120        // Log output for debugging if needed
121        tracing::debug!(output = %output, "OpenTofu apply output");
122
123        Ok(())
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use std::sync::Arc;
130
131    use crate::adapters::tofu::client::OpenTofuClient;
132
133    use super::*;
134
135    #[test]
136    fn it_should_create_apply_infrastructure_step() {
137        let opentofu_client = Arc::new(OpenTofuClient::new("/tmp"));
138
139        let _step = ApplyInfrastructureStep::new(opentofu_client);
140
141        // If we reach this point, the step was created successfully
142    }
143
144    #[test]
145    fn it_should_create_step_with_custom_auto_approve() {
146        let opentofu_client = Arc::new(OpenTofuClient::new("/tmp"));
147
148        let step = ApplyInfrastructureStep::new(opentofu_client).with_auto_approve(false);
149
150        assert!(!step.auto_approve);
151    }
152
153    #[test]
154    fn it_should_default_to_auto_approve() {
155        let opentofu_client = Arc::new(OpenTofuClient::new("/tmp"));
156
157        let step = ApplyInfrastructureStep::new(opentofu_client);
158
159        assert!(step.auto_approve);
160    }
161}