torrust_tracker_deployer_lib/application/services/rendering/ansible.rs
1//! Ansible Template Rendering Service
2//!
3//! This service is responsible for rendering Ansible templates with runtime
4//! configuration. It's used by multiple command handlers (Provision, Register)
5//! to prepare Ansible inventory and playbook files before configuration.
6//!
7//! ## Usage
8//!
9//! The service is injected with its dependencies (template renderer) at construction
10//! time and receives only the data needed to render templates at execution time.
11//!
12//! ```rust,ignore
13//! use torrust_tracker_deployer_lib::application::services::rendering::AnsibleTemplateRenderingService;
14//!
15//! // Create service with dependencies
16//! let service = AnsibleTemplateRenderingService::from_paths(
17//! templates_dir,
18//! build_dir,
19//! clock,
20//! );
21//!
22//! // Render templates with user inputs and instance IP
23//! service.render_templates(&user_inputs, instance_ip, None).await?;
24//! ```
25
26use std::net::{IpAddr, SocketAddr};
27use std::path::PathBuf;
28use std::sync::Arc;
29
30use thiserror::Error;
31use tracing::info;
32
33use crate::application::steps::RenderAnsibleTemplatesStep;
34use crate::domain::environment::UserInputs;
35use crate::domain::TemplateManager;
36use crate::infrastructure::templating::ansible::AnsibleProjectGenerator;
37use crate::shared::clock::Clock;
38
39/// Errors that can occur during Ansible template rendering
40#[derive(Error, Debug)]
41pub enum AnsibleTemplateRenderingServiceError {
42 /// Template rendering failed
43 #[error("Failed to render Ansible templates: {reason}")]
44 RenderingFailed {
45 /// Detailed reason for the failure
46 reason: String,
47 },
48}
49
50/// Service for rendering Ansible templates with runtime configuration
51///
52/// This service encapsulates the logic for rendering Ansible inventory and
53/// configuration templates. It's designed to be shared across command handlers
54/// that need to prepare Ansible files (e.g., Provision, Register).
55///
56/// ## Design
57///
58/// The service follows dependency injection principles:
59/// - Dependencies (template renderer) are injected at construction time
60/// - Runtime data (SSH credentials, IP, port) is passed to the render method
61///
62/// This allows the service to be configured once and reused with different
63/// runtime parameters.
64pub struct AnsibleTemplateRenderingService {
65 ansible_template_renderer: Arc<AnsibleProjectGenerator>,
66 clock: Arc<dyn Clock>,
67}
68
69impl AnsibleTemplateRenderingService {
70 /// Create a new `AnsibleTemplateRenderingService`
71 ///
72 /// # Arguments
73 ///
74 /// * `ansible_template_renderer` - The renderer for Ansible templates
75 /// * `clock` - The clock for generating timestamps
76 #[must_use]
77 pub fn new(
78 ansible_template_renderer: Arc<AnsibleProjectGenerator>,
79 clock: Arc<dyn Clock>,
80 ) -> Self {
81 Self {
82 ansible_template_renderer,
83 clock,
84 }
85 }
86
87 /// Build an `AnsibleTemplateRenderingService` from environment paths
88 ///
89 /// This is a factory method that creates the service with all necessary
90 /// dependencies based on the environment's template and build directories.
91 ///
92 /// # Arguments
93 ///
94 /// * `templates_dir` - Directory containing the source templates
95 /// * `build_dir` - Directory where rendered templates will be written
96 /// * `clock` - The clock for generating timestamps
97 ///
98 /// # Returns
99 ///
100 /// Returns a configured `AnsibleTemplateRenderingService` ready for template rendering
101 ///
102 /// # Example
103 ///
104 /// ```rust,ignore
105 /// use std::path::PathBuf;
106 /// use std::sync::Arc;
107 /// use torrust_tracker_deployer_lib::application::services::rendering::AnsibleTemplateRenderingService;
108 /// use torrust_tracker_deployer_lib::shared::clock::SystemClock;
109 ///
110 /// let service = AnsibleTemplateRenderingService::from_paths(
111 /// PathBuf::from("templates"),
112 /// PathBuf::from("build/my-env"),
113 /// Arc::new(SystemClock),
114 /// );
115 /// ```
116 #[must_use]
117 pub fn from_paths(templates_dir: PathBuf, build_dir: PathBuf, clock: Arc<dyn Clock>) -> Self {
118 let template_manager = Arc::new(TemplateManager::new(templates_dir));
119
120 let ansible_template_renderer =
121 Arc::new(AnsibleProjectGenerator::new(build_dir, template_manager));
122
123 Self::new(ansible_template_renderer, clock)
124 }
125
126 /// Render Ansible templates with the provided runtime configuration
127 ///
128 /// This renders the Ansible inventory and configuration templates so that
129 /// Ansible playbooks can be executed against the target instance.
130 ///
131 /// # Arguments
132 ///
133 /// * `user_inputs` - User-provided environment configuration (SSH credentials, tracker config, etc.)
134 /// * `instance_ip` - IP address of the provisioned instance (runtime output)
135 /// * `ssh_port_override` - Optional SSH port override (takes precedence over `user_inputs.ssh_port`)
136 ///
137 /// # Errors
138 ///
139 /// Returns `AnsibleTemplateRenderingServiceError::RenderingFailed` if template rendering fails.
140 ///
141 /// # Example
142 ///
143 /// ```rust,ignore
144 /// use std::net::IpAddr;
145 ///
146 /// let service = AnsibleTemplateRenderingService::from_paths(...);
147 /// service.render_templates(&user_inputs, "192.168.1.100".parse().unwrap(), None).await?;
148 /// ```
149 pub async fn render_templates(
150 &self,
151 user_inputs: &UserInputs,
152 instance_ip: IpAddr,
153 ssh_port_override: Option<u16>,
154 ) -> Result<(), AnsibleTemplateRenderingServiceError> {
155 let effective_ssh_port = ssh_port_override.unwrap_or(user_inputs.ssh_port());
156
157 info!(
158 instance_ip = %instance_ip,
159 ssh_port = effective_ssh_port,
160 ssh_port_override = ?ssh_port_override,
161 "Rendering Ansible templates"
162 );
163
164 let ssh_socket_addr = SocketAddr::new(instance_ip, effective_ssh_port);
165
166 RenderAnsibleTemplatesStep::new(
167 self.ansible_template_renderer.clone(),
168 user_inputs.ssh_credentials().clone(),
169 ssh_socket_addr,
170 user_inputs.tracker().clone(),
171 user_inputs.grafana().cloned(),
172 self.clock.clone(),
173 )
174 .execute()
175 .await
176 .map_err(|e| AnsibleTemplateRenderingServiceError::RenderingFailed {
177 reason: e.to_string(),
178 })?;
179
180 info!(
181 instance_ip = %instance_ip,
182 ssh_port = effective_ssh_port,
183 "Ansible templates rendered successfully"
184 );
185
186 Ok(())
187 }
188}