Skip to main content

torrust_tracker_deployer_lib/application/steps/rendering/
ansible_templates.rs

1//! Ansible template rendering step
2//!
3//! This module provides the `RenderAnsibleTemplatesStep` which handles rendering
4//! of Ansible configuration templates with runtime variables like IP addresses
5//! and SSH keys. This step prepares Ansible inventory and playbook files for
6//! configuration management operations.
7//!
8//! ## Key Features
9//!
10//! - Dynamic template rendering with runtime variables (IP addresses, SSH keys)
11//! - Ansible inventory generation with host information
12//! - SSH key path processing and validation
13//! - Comprehensive error handling with detailed context
14//!
15//! ## Usage Context
16//!
17//! This step is executed after infrastructure provisioning when instance IP
18//! addresses are known, allowing for the generation of dynamic Ansible
19//! configurations for remote host management.
20
21use std::net::SocketAddr;
22use std::sync::Arc;
23
24use thiserror::Error;
25use tracing::{info, instrument};
26
27use crate::adapters::ssh::credentials::SshCredentials;
28use crate::domain::grafana::GrafanaConfig;
29use crate::domain::tracker::TrackerConfig;
30use crate::infrastructure::templating::ansible::template::renderer::AnsibleProjectGeneratorError;
31use crate::infrastructure::templating::ansible::template::wrappers::inventory::{
32    AnsibleHost, AnsiblePort, AnsiblePortError, InventoryContext, InventoryContextError,
33    SshPrivateKeyFile, SshPrivateKeyFileError,
34};
35use crate::infrastructure::templating::ansible::AnsibleProjectGenerator;
36use crate::infrastructure::templating::TemplateMetadata;
37use crate::shared::clock::Clock;
38
39/// Errors that can occur during Ansible template rendering step execution
40#[derive(Error, Debug)]
41pub enum RenderAnsibleTemplatesError {
42    /// SSH key path parsing failed
43    #[error("SSH key path parsing failed: {0}")]
44    SshKeyPathError(#[from] SshPrivateKeyFileError),
45
46    /// SSH port parsing failed
47    #[error("SSH port parsing failed: {0}")]
48    SshPortError(#[from] AnsiblePortError),
49
50    /// Inventory context creation failed
51    #[error("Inventory context creation failed: {0}")]
52    InventoryContextError(#[from] InventoryContextError),
53
54    /// Template rendering failed
55    #[error("Template rendering failed: {0}")]
56    TemplateRenderingError(#[from] AnsibleProjectGeneratorError),
57}
58
59impl crate::shared::Traceable for RenderAnsibleTemplatesError {
60    fn trace_format(&self) -> String {
61        match self {
62            Self::SshKeyPathError(e) => {
63                format!("RenderAnsibleTemplatesError: SSH key path parsing failed - {e}")
64            }
65            Self::SshPortError(e) => {
66                format!("RenderAnsibleTemplatesError: SSH port parsing failed - {e}")
67            }
68            Self::InventoryContextError(e) => {
69                format!("RenderAnsibleTemplatesError: Inventory context creation failed - {e}")
70            }
71            Self::TemplateRenderingError(e) => {
72                format!("RenderAnsibleTemplatesError: Template rendering failed - {e}")
73            }
74        }
75    }
76
77    fn trace_source(&self) -> Option<&dyn crate::shared::Traceable> {
78        // None of the source errors implement Traceable
79        None
80    }
81
82    fn error_kind(&self) -> crate::shared::ErrorKind {
83        crate::shared::ErrorKind::TemplateRendering
84    }
85}
86
87/// Simple step that renders `Ansible` templates to the build directory with runtime variables
88pub struct RenderAnsibleTemplatesStep {
89    ansible_project_generator: Arc<AnsibleProjectGenerator>,
90    ssh_credentials: SshCredentials,
91    ssh_socket_addr: SocketAddr,
92    tracker_config: TrackerConfig,
93    grafana_config: Option<GrafanaConfig>,
94    clock: Arc<dyn Clock>,
95}
96
97impl RenderAnsibleTemplatesStep {
98    #[must_use]
99    pub fn new(
100        ansible_project_generator: Arc<AnsibleProjectGenerator>,
101        ssh_credentials: SshCredentials,
102        ssh_socket_addr: SocketAddr,
103        tracker_config: TrackerConfig,
104        grafana_config: Option<GrafanaConfig>,
105        clock: Arc<dyn Clock>,
106    ) -> Self {
107        Self {
108            ansible_project_generator,
109            ssh_credentials,
110            ssh_socket_addr,
111            tracker_config,
112            grafana_config,
113            clock,
114        }
115    }
116
117    /// Execute the template rendering step
118    ///
119    /// # Errors
120    ///
121    /// Returns an error if the template rendering fails or if there are issues
122    /// with the template manager or renderer.
123    #[instrument(
124        name = "render_ansible_templates",
125        skip_all,
126        fields(step_type = "rendering", template_type = "ansible")
127    )]
128    pub async fn execute(&self) -> Result<(), RenderAnsibleTemplatesError> {
129        info!(
130            step = "render_ansible_templates",
131            "Rendering Ansible templates with runtime variables"
132        );
133
134        // Create inventory context with runtime variables
135        let inventory_context = self.create_inventory_context()?;
136
137        // Use the configuration renderer to handle all template rendering
138        self.ansible_project_generator
139            .render(
140                &inventory_context,
141                Some(&self.tracker_config),
142                self.grafana_config.as_ref(),
143            )
144            .await?;
145
146        info!(
147            step = "render_ansible_templates",
148            status = "success",
149            "Ansible templates rendered successfully"
150        );
151
152        Ok(())
153    }
154
155    /// Create inventory context with runtime variables from instance data
156    ///
157    /// # Errors
158    ///
159    /// Returns an error if:
160    /// - SSH key path parsing fails
161    /// - Inventory context creation fails
162    fn create_inventory_context(&self) -> Result<InventoryContext, RenderAnsibleTemplatesError> {
163        let metadata = TemplateMetadata::new(self.clock.now());
164        let host = AnsibleHost::from(self.ssh_socket_addr.ip());
165        let ssh_key = SshPrivateKeyFile::new(&self.ssh_credentials.ssh_priv_key_path)?;
166        let ssh_port = AnsiblePort::new(self.ssh_socket_addr.port())?;
167        let ansible_user = self.ssh_credentials.ssh_username.as_str().to_string();
168
169        InventoryContext::builder()
170            .with_metadata(metadata)
171            .with_host(host)
172            .with_ssh_priv_key_path(ssh_key)
173            .with_ssh_port(ssh_port)
174            .with_ansible_user(ansible_user)
175            .build()
176            .map_err(RenderAnsibleTemplatesError::from)
177    }
178}