Skip to content

Commit 91224b5

Browse files
Copilotjosecelano
andcommitted
fix: refactor firewall template to follow established architecture pattern
BREAKING CHANGE: Restructured firewall playbook template rendering - Create proper two-layer template architecture for firewall playbook - Add FirewallPlaybookContext with type-safe SSH port validation - Add FirewallPlaybookTemplate wrapper for template validation - Add FirewallPlaybookTemplateRenderer following inventory pattern - Remove generic render_tera_template method from AnsibleTemplateRenderer - Remove ssh_port field from InventoryContext (use dedicated context) - Fix hosts pattern in configure-firewall.yml.tera (torrust_servers → all) This change ensures consistency with the established template architecture pattern used for inventory.yml.tera, providing better type safety, testability, and maintainability. Addresses all 3 issues from PR review: - Issue #1: Template architecture violation (fixed) - Issue #2: Incorrect Ansible host pattern (fixed) - Issue #3: UFW not active (should be fixed by issue #2) Co-authored-by: josecelano <[email protected]>
1 parent 7d91d18 commit 91224b5

File tree

7 files changed

+731
-84
lines changed

7 files changed

+731
-84
lines changed
Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
//! # Firewall Playbook Template Renderer
2+
//!
3+
//! This module handles rendering of the `configure-firewall.yml.tera` template
4+
//! with SSH port configuration. It's responsible for creating the Ansible playbook
5+
//! that configures UFW firewall while preserving SSH access.
6+
//!
7+
//! ## Responsibilities
8+
//!
9+
//! - Load the `configure-firewall.yml.tera` template file
10+
//! - Process template with SSH port configuration
11+
//! - Render final `configure-firewall.yml` file for Ansible consumption
12+
//!
13+
//! ## Usage
14+
//!
15+
//! ```rust
16+
//! # use std::sync::Arc;
17+
//! # use tempfile::TempDir;
18+
//! use torrust_tracker_deployer_lib::infrastructure::external_tools::ansible::template::renderer::firewall_playbook::FirewallPlaybookTemplateRenderer;
19+
//! use torrust_tracker_deployer_lib::domain::template::TemplateManager;
20+
//! use torrust_tracker_deployer_lib::infrastructure::external_tools::ansible::template::wrappers::firewall_playbook::FirewallPlaybookContext;
21+
//! use torrust_tracker_deployer_lib::infrastructure::external_tools::ansible::template::wrappers::inventory::context::AnsiblePort;
22+
//!
23+
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
24+
//! let temp_dir = TempDir::new()?;
25+
//! let template_manager = Arc::new(TemplateManager::new("/path/to/templates"));
26+
//! let renderer = FirewallPlaybookTemplateRenderer::new(template_manager);
27+
//!
28+
//! let ssh_port = AnsiblePort::new(22)?;
29+
//! let firewall_context = FirewallPlaybookContext::new(ssh_port)?;
30+
//! renderer.render(&firewall_context, temp_dir.path())?;
31+
//! # Ok(())
32+
//! # }
33+
//! ```
34+
35+
use std::path::Path;
36+
use std::sync::Arc;
37+
use thiserror::Error;
38+
39+
use crate::domain::template::file::File;
40+
use crate::domain::template::{FileOperationError, TemplateManager, TemplateManagerError};
41+
use crate::infrastructure::external_tools::ansible::template::wrappers::firewall_playbook::{
42+
FirewallPlaybookContext, FirewallPlaybookTemplate,
43+
};
44+
45+
/// Errors that can occur during firewall playbook template rendering
46+
#[derive(Error, Debug)]
47+
pub enum FirewallPlaybookTemplateError {
48+
/// Failed to get template path from template manager
49+
#[error("Failed to get template path for '{file_name}': {source}")]
50+
TemplatePathFailed {
51+
file_name: String,
52+
#[source]
53+
source: TemplateManagerError,
54+
},
55+
56+
/// Failed to read Tera template file content
57+
#[error("Failed to read Tera template file '{file_name}': {source}")]
58+
TeraTemplateReadFailed {
59+
file_name: String,
60+
#[source]
61+
source: std::io::Error,
62+
},
63+
64+
/// Failed to create File object from template content
65+
#[error("Failed to create File object for '{file_name}': {source}")]
66+
FileCreationFailed {
67+
file_name: String,
68+
#[source]
69+
source: crate::domain::template::file::Error,
70+
},
71+
72+
/// Failed to create firewall playbook template with provided context
73+
#[error("Failed to create FirewallPlaybookTemplate: {source}")]
74+
FirewallPlaybookTemplateCreationFailed {
75+
#[source]
76+
source: crate::domain::template::TemplateEngineError,
77+
},
78+
79+
/// Failed to render firewall playbook template to output file
80+
#[error("Failed to render firewall playbook template to file: {source}")]
81+
FirewallPlaybookTemplateRenderFailed {
82+
#[source]
83+
source: FileOperationError,
84+
},
85+
}
86+
87+
/// Handles rendering of the configure-firewall.yml.tera template for Ansible deployments
88+
///
89+
/// This collaborator is responsible for all firewall playbook template-specific operations:
90+
/// - Loading the configure-firewall.yml.tera template
91+
/// - Processing it with SSH port configuration
92+
/// - Rendering the final configure-firewall.yml file for Ansible consumption
93+
pub struct FirewallPlaybookTemplateRenderer {
94+
template_manager: Arc<TemplateManager>,
95+
}
96+
97+
impl FirewallPlaybookTemplateRenderer {
98+
/// Template filename for the firewall playbook Tera template
99+
const FIREWALL_TEMPLATE_FILE: &'static str = "configure-firewall.yml.tera";
100+
101+
/// Output filename for the rendered firewall playbook file
102+
const FIREWALL_OUTPUT_FILE: &'static str = "configure-firewall.yml";
103+
104+
/// Creates a new firewall playbook template renderer
105+
///
106+
/// # Arguments
107+
///
108+
/// * `template_manager` - The template manager to source templates from
109+
#[must_use]
110+
pub fn new(template_manager: Arc<TemplateManager>) -> Self {
111+
Self { template_manager }
112+
}
113+
114+
/// Renders the configure-firewall.yml.tera template with the provided context
115+
///
116+
/// This method:
117+
/// 1. Loads the configure-firewall.yml.tera template from the template manager
118+
/// 2. Reads the template content
119+
/// 3. Creates a File object for template processing
120+
/// 4. Creates a `FirewallPlaybookTemplate` with the SSH port context
121+
/// 5. Renders the template to configure-firewall.yml in the output directory
122+
///
123+
/// # Arguments
124+
///
125+
/// * `firewall_context` - The context containing SSH port configuration
126+
/// * `output_dir` - The directory where configure-firewall.yml should be written
127+
///
128+
/// # Returns
129+
///
130+
/// * `Result<(), FirewallPlaybookTemplateError>` - Success or error from the template rendering operation
131+
///
132+
/// # Errors
133+
///
134+
/// Returns an error if:
135+
/// - Template file cannot be found or read
136+
/// - Template content is invalid
137+
/// - Variable substitution fails
138+
/// - Output file cannot be written
139+
pub fn render(
140+
&self,
141+
firewall_context: &FirewallPlaybookContext,
142+
output_dir: &Path,
143+
) -> Result<(), FirewallPlaybookTemplateError> {
144+
tracing::debug!("Rendering firewall playbook template with SSH port configuration");
145+
146+
// Get the firewall playbook template path
147+
let firewall_template_path = self
148+
.template_manager
149+
.get_template_path(&Self::build_template_path())
150+
.map_err(|source| FirewallPlaybookTemplateError::TemplatePathFailed {
151+
file_name: Self::FIREWALL_TEMPLATE_FILE.to_string(),
152+
source,
153+
})?;
154+
155+
// Read template content
156+
let firewall_template_content =
157+
std::fs::read_to_string(&firewall_template_path).map_err(|source| {
158+
FirewallPlaybookTemplateError::TeraTemplateReadFailed {
159+
file_name: Self::FIREWALL_TEMPLATE_FILE.to_string(),
160+
source,
161+
}
162+
})?;
163+
164+
// Create File object for template processing
165+
let firewall_template_file =
166+
File::new(Self::FIREWALL_TEMPLATE_FILE, firewall_template_content).map_err(
167+
|source| FirewallPlaybookTemplateError::FileCreationFailed {
168+
file_name: Self::FIREWALL_TEMPLATE_FILE.to_string(),
169+
source,
170+
},
171+
)?;
172+
173+
// Create FirewallPlaybookTemplate with SSH port context
174+
let firewall_template =
175+
FirewallPlaybookTemplate::new(&firewall_template_file, firewall_context.clone())
176+
.map_err(|source| {
177+
FirewallPlaybookTemplateError::FirewallPlaybookTemplateCreationFailed { source }
178+
})?;
179+
180+
// Render to output file
181+
let firewall_output_path = output_dir.join(Self::FIREWALL_OUTPUT_FILE);
182+
firewall_template
183+
.render(&firewall_output_path)
184+
.map_err(|source| {
185+
FirewallPlaybookTemplateError::FirewallPlaybookTemplateRenderFailed { source }
186+
})?;
187+
188+
tracing::debug!(
189+
"Successfully rendered firewall playbook template to {}",
190+
firewall_output_path.display()
191+
);
192+
193+
Ok(())
194+
}
195+
196+
/// Builds the full template path for the firewall playbook template
197+
///
198+
/// # Returns
199+
///
200+
/// * `String` - The complete template path for configure-firewall.yml.tera
201+
fn build_template_path() -> String {
202+
format!("ansible/{}", Self::FIREWALL_TEMPLATE_FILE)
203+
}
204+
}
205+
206+
#[cfg(test)]
207+
mod tests {
208+
use super::*;
209+
use crate::infrastructure::external_tools::ansible::template::wrappers::inventory::context::AnsiblePort;
210+
use std::fs;
211+
use tempfile::TempDir;
212+
213+
/// Helper function to create a test firewall context
214+
fn create_test_firewall_context() -> FirewallPlaybookContext {
215+
let ssh_port = AnsiblePort::new(22).expect("Failed to create SSH port");
216+
FirewallPlaybookContext::builder()
217+
.with_ssh_port(ssh_port)
218+
.build()
219+
.expect("Failed to build firewall context")
220+
}
221+
222+
/// Helper function to create a test template directory with configure-firewall.yml.tera
223+
fn create_test_templates(temp_dir: &Path) -> std::io::Result<()> {
224+
let ansible_dir = temp_dir.join("ansible");
225+
fs::create_dir_all(&ansible_dir)?;
226+
227+
let template_content = r#"---
228+
- name: Configure UFW firewall
229+
hosts: all
230+
become: yes
231+
tasks:
232+
- name: Allow SSH on port {{ssh_port}}
233+
community.general.ufw:
234+
rule: allow
235+
port: "{{ssh_port}}"
236+
proto: tcp
237+
"#;
238+
239+
fs::write(
240+
ansible_dir.join("configure-firewall.yml.tera"),
241+
template_content,
242+
)?;
243+
244+
Ok(())
245+
}
246+
247+
#[test]
248+
fn it_should_create_firewall_renderer_with_template_manager() {
249+
let temp_dir = TempDir::new().expect("Failed to create temp directory");
250+
let template_manager = Arc::new(TemplateManager::new(temp_dir.path()));
251+
252+
let renderer = FirewallPlaybookTemplateRenderer::new(template_manager.clone());
253+
254+
assert!(Arc::ptr_eq(&renderer.template_manager, &template_manager));
255+
}
256+
257+
#[test]
258+
fn it_should_build_correct_template_path() {
259+
let temp_dir = TempDir::new().expect("Failed to create temp directory");
260+
let template_manager = Arc::new(TemplateManager::new(temp_dir.path()));
261+
let _renderer = FirewallPlaybookTemplateRenderer::new(template_manager);
262+
263+
let template_path = FirewallPlaybookTemplateRenderer::build_template_path();
264+
265+
assert_eq!(template_path, "ansible/configure-firewall.yml.tera");
266+
}
267+
268+
#[test]
269+
fn it_should_render_firewall_template_successfully() {
270+
let temp_dir = TempDir::new().expect("Failed to create temp directory");
271+
let template_dir = temp_dir.path().join("templates");
272+
let output_dir = temp_dir.path().join("output");
273+
274+
// Create template directory and files
275+
create_test_templates(&template_dir).expect("Failed to create test templates");
276+
fs::create_dir_all(&output_dir).expect("Failed to create output directory");
277+
278+
// Setup template manager and renderer
279+
let template_manager = Arc::new(TemplateManager::new(&template_dir));
280+
template_manager
281+
.ensure_templates_dir()
282+
.expect("Failed to ensure templates directory");
283+
284+
let renderer = FirewallPlaybookTemplateRenderer::new(template_manager);
285+
let firewall_context = create_test_firewall_context();
286+
287+
// Render template
288+
let result = renderer.render(&firewall_context, &output_dir);
289+
290+
assert!(result.is_ok(), "Template rendering should succeed");
291+
292+
// Verify output file exists
293+
let output_file = output_dir.join("configure-firewall.yml");
294+
assert!(
295+
output_file.exists(),
296+
"configure-firewall.yml should be created"
297+
);
298+
299+
// Verify output content contains expected values
300+
let output_content = fs::read_to_string(&output_file).expect("Failed to read output file");
301+
assert!(
302+
output_content.contains("22"),
303+
"Output should contain the SSH port"
304+
);
305+
assert!(
306+
output_content.contains("hosts: all"),
307+
"Output should contain hosts: all"
308+
);
309+
assert!(
310+
!output_content.contains("{{ssh_port}}"),
311+
"Output should not contain template variables"
312+
);
313+
}
314+
315+
#[test]
316+
fn it_should_render_with_custom_ssh_port() {
317+
let temp_dir = TempDir::new().expect("Failed to create temp directory");
318+
let template_dir = temp_dir.path().join("templates");
319+
let output_dir = temp_dir.path().join("output");
320+
321+
create_test_templates(&template_dir).expect("Failed to create test templates");
322+
fs::create_dir_all(&output_dir).expect("Failed to create output directory");
323+
324+
let template_manager = Arc::new(TemplateManager::new(&template_dir));
325+
template_manager
326+
.ensure_templates_dir()
327+
.expect("Failed to ensure templates directory");
328+
329+
let renderer = FirewallPlaybookTemplateRenderer::new(template_manager);
330+
331+
// Use custom SSH port
332+
let ssh_port = AnsiblePort::new(2222).expect("Failed to create SSH port");
333+
let firewall_context =
334+
FirewallPlaybookContext::new(ssh_port).expect("Failed to create context");
335+
336+
let result = renderer.render(&firewall_context, &output_dir);
337+
338+
assert!(result.is_ok());
339+
340+
let output_file = output_dir.join("configure-firewall.yml");
341+
let output_content = fs::read_to_string(&output_file).expect("Failed to read output file");
342+
assert!(
343+
output_content.contains("2222"),
344+
"Output should contain custom SSH port 2222"
345+
);
346+
}
347+
}

0 commit comments

Comments
 (0)