Skip to main content

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}