Skip to main content

torrust_tracker_deployer_lib/application/services/rendering/
backup.rs

1//! Backup template rendering service
2//!
3//! This service handles rendering of backup configuration templates,
4//! including database configuration conversion and schedule handling.
5
6use std::path::PathBuf;
7use std::sync::Arc;
8
9use chrono::{DateTime, Utc};
10use tracing::{info, instrument};
11
12use crate::domain::backup::BackupConfig;
13use crate::domain::tracker::DatabaseConfig;
14use crate::domain::TemplateManager;
15use crate::infrastructure::templating::backup::template::wrapper::backup_config::context::{
16    BackupContext, BackupDatabaseConfig,
17};
18use crate::infrastructure::templating::backup::{
19    BackupProjectGenerator, BackupProjectGeneratorError,
20};
21use crate::infrastructure::templating::TemplateMetadata;
22
23/// Service for rendering backup configuration templates
24///
25/// This service encapsulates the logic for rendering backup configurations,
26/// including:
27/// - Converting domain `DatabaseConfig` to template `BackupDatabaseConfig`
28/// - Building `BackupContext` with schedule information
29/// - Conditional rendering (only when backup is configured)
30pub struct BackupTemplateRenderingService {
31    templates_dir: PathBuf,
32    build_dir: PathBuf,
33}
34
35impl BackupTemplateRenderingService {
36    /// Create a new service with explicit dependencies
37    ///
38    /// # Arguments
39    ///
40    /// * `templates_dir` - Directory containing template source files
41    /// * `build_dir` - Directory where rendered templates will be written
42    #[must_use]
43    pub fn from_paths(templates_dir: PathBuf, build_dir: PathBuf) -> Self {
44        Self {
45            templates_dir,
46            build_dir,
47        }
48    }
49
50    /// Render backup templates if backup is configured
51    ///
52    /// This method converts the domain database configuration to the backup
53    /// format and renders the backup configuration templates. Returns `None`
54    /// if backup is not configured.
55    ///
56    /// # Arguments
57    ///
58    /// * `backup_config` - Optional backup configuration
59    /// * `database_config` - Tracker database configuration
60    /// * `created_at` - Environment creation timestamp
61    ///
62    /// # Returns
63    ///
64    /// `Some(PathBuf)` with path to the rendered backup build directory if
65    /// backup is configured, or `None` if backup should not be deployed.
66    ///
67    /// # Errors
68    ///
69    /// Returns error if template rendering fails
70    #[instrument(
71        name = "backup_rendering_service",
72        skip_all,
73        fields(
74            templates_dir = %self.templates_dir.display(),
75            build_dir = %self.build_dir.display()
76        )
77    )]
78    pub async fn render(
79        &self,
80        backup_config: Option<&BackupConfig>,
81        database_config: &DatabaseConfig,
82        created_at: DateTime<Utc>,
83    ) -> Result<Option<PathBuf>, BackupTemplateRenderingServiceError> {
84        // Check if backup configuration exists
85        let Some(backup_config) = backup_config else {
86            info!(
87                reason = "backup_not_configured",
88                "Skipping backup template rendering - backup not configured"
89            );
90            return Ok(None);
91        };
92
93        info!(
94            templates_dir = %self.templates_dir.display(),
95            build_dir = %self.build_dir.display(),
96            "Rendering backup configuration templates"
97        );
98
99        let template_manager = Arc::new(TemplateManager::new(self.templates_dir.clone()));
100        let generator = BackupProjectGenerator::new(self.build_dir.clone(), template_manager);
101
102        let backup_database_config = convert_database_config_to_backup(database_config);
103        let metadata = TemplateMetadata::new(created_at);
104        let context = BackupContext::from_config(metadata, backup_config, backup_database_config);
105
106        generator
107            .render(&context, backup_config.schedule())
108            .await
109            .map_err(BackupTemplateRenderingServiceError::RenderingFailed)?;
110
111        let backup_dir_path = self.build_dir.join("backup/etc");
112
113        info!(
114            backup_dir_path = %backup_dir_path.display(),
115            "Backup templates rendered successfully"
116        );
117
118        Ok(Some(backup_dir_path))
119    }
120}
121
122/// Converts domain `DatabaseConfig` to template `BackupDatabaseConfig`
123///
124/// Maps the domain database configuration (used for tracker setup) to the
125/// backup-specific database configuration format (used for backup script generation).
126fn convert_database_config_to_backup(config: &DatabaseConfig) -> BackupDatabaseConfig {
127    match config {
128        DatabaseConfig::Sqlite(sqlite_config) => BackupDatabaseConfig::Sqlite {
129            path: format!(
130                "/data/storage/tracker/lib/database/{}",
131                sqlite_config.database_name()
132            ),
133        },
134        DatabaseConfig::Mysql(mysql_config) => BackupDatabaseConfig::Mysql {
135            host: mysql_config.host().to_string(),
136            port: mysql_config.port(),
137            database: mysql_config.database_name().to_string(),
138            user: mysql_config.username().to_string(),
139            password: mysql_config.password().expose_secret().to_string(),
140        },
141    }
142}
143
144/// Errors that can occur during backup template rendering
145#[derive(Debug, thiserror::Error)]
146pub enum BackupTemplateRenderingServiceError {
147    /// Template rendering failed
148    #[error("Backup template rendering failed: {0}")]
149    RenderingFailed(#[from] BackupProjectGeneratorError),
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use tempfile::TempDir;
156
157    use crate::domain::backup::BackupConfig;
158    use crate::domain::tracker::{DatabaseConfig, SqliteConfig};
159
160    #[tokio::test]
161    async fn it_should_create_service_with_from_paths() {
162        let templates_dir = TempDir::new().expect("Failed to create temp dir");
163        let build_dir = TempDir::new().expect("Failed to create temp dir");
164
165        let service = BackupTemplateRenderingService::from_paths(
166            templates_dir.path().to_path_buf(),
167            build_dir.path().to_path_buf(),
168        );
169
170        assert_eq!(service.templates_dir, templates_dir.path());
171        assert_eq!(service.build_dir, build_dir.path());
172    }
173
174    #[tokio::test]
175    async fn it_should_return_none_when_backup_not_configured() {
176        let templates_dir = TempDir::new().expect("Failed to create temp dir");
177        let build_dir = TempDir::new().expect("Failed to create temp dir");
178
179        let service = BackupTemplateRenderingService::from_paths(
180            templates_dir.path().to_path_buf(),
181            build_dir.path().to_path_buf(),
182        );
183
184        let database_config = DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap());
185        let created_at = Utc::now();
186
187        let result = service.render(None, &database_config, created_at).await;
188
189        assert!(result.is_ok());
190        assert!(result.unwrap().is_none());
191    }
192
193    #[tokio::test]
194    async fn it_should_render_backup_templates_when_backup_is_configured() {
195        let templates_dir = TempDir::new().expect("Failed to create temp dir");
196        let build_dir = TempDir::new().expect("Failed to create temp dir");
197
198        let service = BackupTemplateRenderingService::from_paths(
199            templates_dir.path().to_path_buf(),
200            build_dir.path().to_path_buf(),
201        );
202
203        let backup_config = BackupConfig::default();
204        let database_config = DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap());
205        let created_at = Utc::now();
206
207        let result = service
208            .render(Some(&backup_config), &database_config, created_at)
209            .await;
210
211        assert!(result.is_ok());
212        let backup_dir = result.unwrap();
213        assert!(backup_dir.is_some());
214        let backup_dir = backup_dir.unwrap();
215        assert!(backup_dir.to_string_lossy().contains("backup/etc"));
216    }
217
218    #[test]
219    fn it_should_convert_sqlite_config_to_backup_format() {
220        let sqlite_config = SqliteConfig::new("tracker.db").unwrap();
221        let database_config = DatabaseConfig::Sqlite(sqlite_config.clone());
222
223        let backup_config = convert_database_config_to_backup(&database_config);
224
225        match backup_config {
226            BackupDatabaseConfig::Sqlite { path } => {
227                assert!(path.contains(sqlite_config.database_name()));
228            }
229            BackupDatabaseConfig::Mysql { .. } => {
230                panic!("Expected Sqlite config");
231            }
232        }
233    }
234}