Skip to main content

torrust_tracker_deployer_lib/application/steps/rendering/
prometheus_templates.rs

1//! Prometheus template rendering step
2//!
3//! This module provides the `RenderPrometheusTemplatesStep` which handles rendering
4//! of Prometheus configuration templates to the build directory. This step prepares
5//! Prometheus configuration files for deployment to the remote host.
6//!
7//! ## Key Features
8//!
9//! - Template rendering for Prometheus configurations
10//! - Integration with the `PrometheusProjectGenerator` for file generation
11//! - Build directory preparation for deployment operations
12//! - Comprehensive error handling for template processing
13//!
14//! ## Usage Context
15//!
16//! This step is typically executed during the release workflow, after
17//! infrastructure provisioning and software installation, to prepare
18//! the Prometheus configuration files for deployment.
19//!
20//! ## Architecture
21//!
22//! This step follows the three-level architecture:
23//! - **Command** (Level 1): `ReleaseCommandHandler` orchestrates the release workflow
24//! - **Step** (Level 2): This `RenderPrometheusTemplatesStep` handles template rendering
25//! - The templates are rendered locally, no remote action is needed
26
27use std::path::PathBuf;
28use std::sync::Arc;
29
30use tracing::{info, instrument};
31
32use crate::application::services::rendering::PrometheusTemplateRenderingService;
33use crate::application::services::rendering::PrometheusTemplateRenderingServiceError;
34use crate::domain::environment::Environment;
35use crate::shared::clock::Clock;
36
37/// Step that renders Prometheus templates to the build directory
38///
39/// This step handles the preparation of Prometheus configuration files
40/// by rendering templates to the build directory. The rendered files are
41/// then ready to be deployed to the remote host.
42pub struct RenderPrometheusTemplatesStep<S> {
43    environment: Arc<Environment<S>>,
44    templates_dir: PathBuf,
45    build_dir: PathBuf,
46    clock: Arc<dyn Clock>,
47}
48
49impl<S> RenderPrometheusTemplatesStep<S> {
50    /// Creates a new `RenderPrometheusTemplatesStep`
51    ///
52    /// # Arguments
53    ///
54    /// * `environment` - The deployment environment
55    /// * `templates_dir` - The templates directory
56    /// * `build_dir` - The build directory where templates will be rendered
57    /// * `clock` - Clock service for generating timestamps
58    #[must_use]
59    pub fn new(
60        environment: Arc<Environment<S>>,
61        templates_dir: PathBuf,
62        build_dir: PathBuf,
63        clock: Arc<dyn Clock>,
64    ) -> Self {
65        Self {
66            environment,
67            templates_dir,
68            build_dir,
69            clock,
70        }
71    }
72
73    /// Execute the template rendering step
74    ///
75    /// This will render Prometheus templates to the build directory if Prometheus
76    /// configuration is present in the environment.
77    ///
78    /// # Returns
79    ///
80    /// Returns the path to the Prometheus build directory on success, or `None`
81    /// if Prometheus is not configured.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error if:
86    /// * Template rendering fails
87    /// * Directory creation fails
88    /// * File writing fails
89    #[instrument(
90        name = "render_prometheus_templates",
91        skip_all,
92        fields(
93            step_type = "rendering",
94            template_type = "prometheus",
95            build_dir = %self.build_dir.display()
96        )
97    )]
98    pub fn execute(&self) -> Result<Option<PathBuf>, PrometheusTemplateRenderingServiceError> {
99        // Check if Prometheus is configured
100        let Some(prometheus_config) = self.environment.context().user_inputs.prometheus() else {
101            info!(
102                step = "render_prometheus_templates",
103                status = "skipped",
104                reason = "prometheus_not_configured",
105                "Skipping Prometheus template rendering - not configured"
106            );
107            return Ok(None);
108        };
109
110        info!(
111            step = "render_prometheus_templates",
112            templates_dir = %self.templates_dir.display(),
113            build_dir = %self.build_dir.display(),
114            "Rendering Prometheus configuration templates"
115        );
116
117        let service = PrometheusTemplateRenderingService::from_paths(
118            self.templates_dir.clone(),
119            self.build_dir.clone(),
120            self.clock.clone(),
121        );
122
123        // Extract tracker config for API token and port
124        let tracker_config = self.environment.context().user_inputs.tracker();
125        let Some(prometheus_build_dir) = service.render(Some(prometheus_config), tracker_config)?
126        else {
127            return Ok(None);
128        };
129
130        info!(
131            step = "render_prometheus_templates",
132            prometheus_build_dir = %prometheus_build_dir.display(),
133            status = "success",
134            "Prometheus templates rendered successfully"
135        );
136
137        Ok(Some(prometheus_build_dir))
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use tempfile::TempDir;
144
145    use super::*;
146    use crate::domain::environment::testing::EnvironmentTestBuilder;
147    use crate::domain::prometheus::PrometheusConfig;
148    use crate::testing::mock_clock::MockClock;
149
150    fn create_test_clock() -> Arc<dyn Clock> {
151        use chrono::TimeZone;
152        let fixed_time = chrono::Utc
153            .with_ymd_and_hms(2026, 1, 27, 13, 41, 56)
154            .unwrap();
155        Arc::new(MockClock::new(fixed_time))
156    }
157
158    #[test]
159    fn it_should_create_render_prometheus_templates_step() {
160        let templates_dir = TempDir::new().expect("Failed to create templates dir");
161        let build_dir = TempDir::new().expect("Failed to create build dir");
162
163        let (environment, _, _, _temp_dir) =
164            EnvironmentTestBuilder::new().build_with_custom_paths();
165        let environment = Arc::new(environment);
166
167        let clock = create_test_clock();
168        let step = RenderPrometheusTemplatesStep::new(
169            environment.clone(),
170            templates_dir.path().to_path_buf(),
171            build_dir.path().to_path_buf(),
172            clock,
173        );
174
175        assert_eq!(step.build_dir, build_dir.path());
176        assert_eq!(step.templates_dir, templates_dir.path());
177    }
178
179    #[test]
180    fn it_should_skip_rendering_when_prometheus_not_configured() {
181        let templates_dir = TempDir::new().expect("Failed to create templates dir");
182        let build_dir = TempDir::new().expect("Failed to create build dir");
183
184        // Build environment without Prometheus config
185        let (environment, _, _, _temp_dir) = EnvironmentTestBuilder::new()
186            .with_prometheus_config(None)
187            .build_with_custom_paths();
188        let environment = Arc::new(environment);
189
190        let clock = create_test_clock();
191        let step = RenderPrometheusTemplatesStep::new(
192            environment,
193            templates_dir.path().to_path_buf(),
194            build_dir.path().to_path_buf(),
195            clock,
196        );
197
198        let result = step.execute();
199        assert!(
200            result.is_ok(),
201            "Should succeed when Prometheus not configured"
202        );
203        assert!(
204            result.unwrap().is_none(),
205            "Should return None when Prometheus not configured"
206        );
207    }
208
209    #[test]
210    fn it_should_render_templates_when_prometheus_configured() {
211        let templates_dir = TempDir::new().expect("Failed to create templates dir");
212        let build_dir = TempDir::new().expect("Failed to create build dir");
213
214        // Build environment with Prometheus config
215        let (environment, _, _, _temp_dir) = EnvironmentTestBuilder::new()
216            .with_prometheus_config(Some(PrometheusConfig::new(
217                std::num::NonZeroU32::new(30).expect("30 is non-zero"),
218            )))
219            .build_with_custom_paths();
220        let environment = Arc::new(environment);
221
222        let clock = create_test_clock();
223        let step = RenderPrometheusTemplatesStep::new(
224            environment,
225            templates_dir.path().to_path_buf(),
226            build_dir.path().to_path_buf(),
227            clock,
228        );
229
230        let result = step.execute();
231        assert!(result.is_ok(), "Should render Prometheus templates");
232
233        let prometheus_build_dir = result.unwrap();
234        assert!(
235            prometheus_build_dir.is_some(),
236            "Should return build directory path"
237        );
238
239        let build_dir_path = prometheus_build_dir.unwrap();
240        assert!(
241            build_dir_path.to_string_lossy().contains("prometheus"),
242            "Build directory should contain 'prometheus' in path"
243        );
244    }
245}