Skip to content

Commit ad4ead6

Browse files
committed
feat: [#315] Step 3.2 & 3.3 - Crontab installation and wiring
This completes Steps 3.2 and 3.3 of Phase 3: Step 3.2: Add crontab installation playbook - Created install-backup-crontab.yml Ansible playbook - Copies maintenance-backup.sh to /usr/local/bin/ (mode 0755, root:root) - Installs crontab entry to /etc/cron.d/tracker-backup (mode 0644, root:root) - Creates /var/log/tracker-backup.log (mode 0644, root:root) - Registered in ProjectGenerator Step 3.3: Wire crontab into Release command - Created InstallBackupCrontabStep system step module - Added to backup release workflow (after config deployment) - Conditional execution (only if backup enabled in environment) - Updated ReleaseStep enum with InstallBackupCrontab variant - Added comprehensive error variant with help text Integration: - Release workflow: Create Storage → Deploy Config → Install Crontab → Render Compose → Deploy Compose - All services remain healthy during backup operations - Crontab isolated from Docker Compose setup via profiles
1 parent 9c366ab commit ad4ead6

10 files changed

Lines changed: 301 additions & 6 deletions

File tree

src/application/command_handlers/release/errors.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,18 @@ pub enum ReleaseCommandHandlerError {
146146
step: ReleaseStep,
147147
},
148148

149+
/// Backup crontab installation failed
150+
#[error("Backup crontab installation failed: {message}")]
151+
InstallBackupCrontabFailed {
152+
/// Description of the failure
153+
message: String,
154+
/// The underlying error from the installation step
155+
#[source]
156+
source: BoxedStepError,
157+
/// The release step that failed
158+
step: ReleaseStep,
159+
},
160+
149161
/// Backup storage directory creation failed
150162
#[error("Backup storage creation failed: {message}")]
151163
CreateBackupStorageFailed {
@@ -262,6 +274,11 @@ impl Traceable for ReleaseCommandHandlerError {
262274
Self::DeployBackupConfigFailed { message, .. } => {
263275
format!("ReleaseCommandHandlerError: Backup configuration deployment failed - {message}")
264276
}
277+
Self::InstallBackupCrontabFailed { message, .. } => {
278+
format!(
279+
"ReleaseCommandHandlerError: Backup crontab installation failed - {message}"
280+
)
281+
}
265282
Self::CaddyConfigDeployment { message, .. } => {
266283
format!(
267284
"ReleaseCommandHandlerError: Caddy configuration deployment failed - {message}"
@@ -311,6 +328,7 @@ impl Traceable for ReleaseCommandHandlerError {
311328
| Self::RenderBackupTemplatesFailed { .. }
312329
| Self::CreateBackupStorageFailed { .. }
313330
| Self::DeployBackupConfigFailed { .. }
331+
| Self::InstallBackupCrontabFailed { .. }
314332
| Self::CaddyConfigDeployment { .. }
315333
| Self::TrackerConfigDeployment { .. }
316334
| Self::GrafanaProvisioningDeployment { .. }
@@ -335,6 +353,7 @@ impl Traceable for ReleaseCommandHandlerError {
335353
| Self::RenderBackupTemplatesFailed { .. }
336354
| Self::CreateBackupStorageFailed { .. }
337355
| Self::DeployBackupConfigFailed { .. }
356+
| Self::InstallBackupCrontabFailed { .. }
338357
| Self::CaddyConfigDeployment { .. }
339358
| Self::TrackerConfigDeployment { .. }
340359
| Self::GrafanaProvisioningDeployment { .. }
@@ -622,6 +641,37 @@ For more information, see docs/user-guide/commands.md"
622641
623642
4. Check file permissions on remote host
624643
5. Review Ansible playbook execution logs above"
644+
}
645+
Self::InstallBackupCrontabFailed { .. } => {
646+
"Backup Crontab Installation Failed - Troubleshooting:
647+
648+
1. Verify SSH connection to remote host:
649+
ssh <user>@<host>
650+
651+
2. Check Ansible playbook exists:
652+
ls templates/ansible/install-backup-crontab.yml
653+
654+
3. Verify maintenance script is in build directory:
655+
ls build/<env-name>/backup/etc/maintenance-backup.sh
656+
657+
4. Verify crontab entry is generated:
658+
ls build/<env-name>/backup/etc/maintenance-backup.cron
659+
660+
5. Check that cron daemon is running on target:
661+
ssh <user>@<host> 'systemctl status cron'
662+
663+
6. Check file permissions and ownership on target:
664+
ssh <user>@<host> 'ls -la /usr/local/bin/maintenance-backup.sh'
665+
ssh <user>@<host> 'ls -la /etc/cron.d/tracker-backup'
666+
667+
Common causes:
668+
- Cron daemon not installed or running
669+
- Permission denied on cron directory
670+
- Insufficient disk space on target
671+
- SSH authentication failure
672+
- Ansible playbook not found
673+
674+
For more information, see docs/user-guide/commands.md"
625675
}
626676
Self::CaddyConfigDeployment { .. } => {
627677
"Caddy Configuration Deployment Failed - Troubleshooting:

src/application/command_handlers/release/steps/backup.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::application::command_handlers::common::StepResult;
1313
use crate::application::command_handlers::release::errors::ReleaseCommandHandlerError;
1414
use crate::application::steps::application::{CreateBackupStorageStep, DeployBackupConfigStep};
1515
use crate::application::steps::rendering::RenderBackupTemplatesStep;
16+
use crate::application::steps::system::InstallBackupCrontabStep;
1617
use crate::domain::environment::state::ReleaseStep;
1718
use crate::domain::environment::{Environment, Releasing};
1819
use crate::domain::template::TemplateManager;
@@ -50,6 +51,7 @@ pub async fn release(
5051
render_templates(environment).await?;
5152
create_storage(environment)?;
5253
deploy_config_to_remote(environment)?;
54+
install_crontab(environment)?;
5355

5456
Ok(())
5557
}
@@ -157,3 +159,39 @@ fn deploy_config_to_remote(
157159

158160
Ok(())
159161
}
162+
163+
/// Install backup crontab and maintenance script on the remote host
164+
///
165+
/// This installs the cron job that will execute backups on the configured schedule.
166+
/// The cron daemon is always running, so the job will automatically execute on schedule.
167+
///
168+
/// # Errors
169+
///
170+
/// Returns a tuple of (error, `ReleaseStep::InstallBackupCrontab`) if installation fails
171+
#[allow(clippy::result_large_err)]
172+
fn install_crontab(
173+
environment: &Environment<Releasing>,
174+
) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> {
175+
let current_step = ReleaseStep::InstallBackupCrontab;
176+
177+
InstallBackupCrontabStep::new(ansible_client(environment))
178+
.execute()
179+
.map_err(|e| {
180+
(
181+
ReleaseCommandHandlerError::InstallBackupCrontabFailed {
182+
message: e.to_string(),
183+
source: Box::new(e),
184+
step: current_step,
185+
},
186+
current_step,
187+
)
188+
})?;
189+
190+
info!(
191+
command = "release",
192+
step = %current_step,
193+
"Backup crontab and maintenance script installed successfully"
194+
);
195+
196+
Ok(())
197+
}

src/application/steps/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ pub use rendering::{
3838
RenderDockerComposeTemplatesStep, RenderOpenTofuTemplatesStep,
3939
};
4040
pub use software::{InstallDockerComposeStep, InstallDockerStep};
41-
pub use system::{ConfigureFirewallStep, ConfigureSecurityUpdatesStep, WaitForCloudInitStep};
41+
pub use system::{
42+
ConfigureFirewallStep, ConfigureSecurityUpdatesStep, InstallBackupCrontabStep,
43+
WaitForCloudInitStep,
44+
};
4245
pub use validation::{
4346
ValidateCloudInitCompletionStep, ValidateDockerComposeInstallationStep,
4447
ValidateDockerInstallationStep,
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//! Backup crontab installation step
2+
//!
3+
//! This module provides the `InstallBackupCrontabStep` which handles installation
4+
//! of the backup crontab entry and maintenance script on remote hosts via Ansible playbooks.
5+
//! This step ensures that scheduled backups are configured to run automatically.
6+
//!
7+
//! ## Key Features
8+
//!
9+
//! - Copies maintenance-backup.sh to /usr/local/bin/ with executable permissions
10+
//! - Installs crontab entry to /etc/cron.d/tracker-backup
11+
//! - Creates backup log file with proper permissions
12+
//! - Verifies all files are properly installed
13+
//!
14+
//! ## Configuration Process
15+
//!
16+
//! The step executes the "install-backup-crontab" Ansible playbook which handles:
17+
//! - Copying the maintenance script to /usr/local/bin/
18+
//! - Installing the crontab entry to /etc/cron.d/
19+
//! - Creating the backup log file
20+
//! - Verifying all files exist and have correct permissions
21+
22+
use std::sync::Arc;
23+
use tracing::{info, instrument};
24+
25+
use crate::adapters::ansible::AnsibleClient;
26+
use crate::shared::command::CommandError;
27+
28+
/// Step that installs backup crontab and maintenance script via Ansible
29+
///
30+
/// This step installs the backup crontab entry and the maintenance script
31+
/// that will orchestrate scheduled backups. The crontab entry runs on the
32+
/// configured schedule to stop the tracker, perform backup, and restart.
33+
pub struct InstallBackupCrontabStep {
34+
ansible_client: Arc<AnsibleClient>,
35+
}
36+
37+
impl InstallBackupCrontabStep {
38+
/// Create a new backup crontab installation step
39+
///
40+
/// # Arguments
41+
///
42+
/// * `ansible_client` - Ansible client for running playbooks
43+
#[must_use]
44+
pub fn new(ansible_client: Arc<AnsibleClient>) -> Self {
45+
Self { ansible_client }
46+
}
47+
48+
/// Execute the backup crontab installation
49+
///
50+
/// # Errors
51+
///
52+
/// Returns `CommandError` if:
53+
/// - Ansible playbook execution fails
54+
/// - Files cannot be copied to remote host
55+
/// - Permissions cannot be set correctly
56+
/// - Verification checks fail
57+
#[instrument(
58+
name = "install_backup_crontab",
59+
skip_all,
60+
fields(step_type = "system", component = "backup", method = "ansible")
61+
)]
62+
pub fn execute(&self) -> Result<(), CommandError> {
63+
info!(
64+
step = "install_backup_crontab",
65+
action = "install_crontab",
66+
"Installing backup crontab and maintenance script"
67+
);
68+
69+
match self
70+
.ansible_client
71+
.run_playbook("install-backup-crontab", &[])
72+
{
73+
Ok(_) => {
74+
info!(
75+
step = "install_backup_crontab",
76+
action = "install_crontab",
77+
status = "completed",
78+
"Backup crontab and script installed successfully"
79+
);
80+
Ok(())
81+
}
82+
Err(e) => Err(e),
83+
}
84+
}
85+
}
86+
87+
#[cfg(test)]
88+
mod tests {
89+
use super::*;
90+
use crate::adapters::ansible::AnsibleClient;
91+
use std::path::PathBuf;
92+
use std::sync::Arc;
93+
94+
#[test]
95+
fn it_should_create_step_with_ansible_client() {
96+
let build_dir = PathBuf::from("/tmp/test-build");
97+
let ansible_client = Arc::new(AnsibleClient::new(build_dir));
98+
let step = InstallBackupCrontabStep::new(ansible_client);
99+
assert!(Arc::strong_count(&step.ansible_client) >= 1);
100+
}
101+
}

src/application/steps/system/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* - Cloud-init completion waiting
99
* - Automatic security updates configuration
1010
* - UFW firewall configuration (SSH access only)
11+
* - Backup crontab installation
1112
*
1213
* Note: Tracker service ports are controlled via Docker port bindings in docker-compose,
1314
* not through UFW rules. Docker bypasses UFW for published container ports.
@@ -21,8 +22,10 @@
2122

2223
pub mod configure_firewall;
2324
pub mod configure_security_updates;
25+
pub mod install_backup_crontab;
2426
pub mod wait_cloud_init;
2527

2628
pub use configure_firewall::ConfigureFirewallStep;
2729
pub use configure_security_updates::ConfigureSecurityUpdatesStep;
30+
pub use install_backup_crontab::InstallBackupCrontabStep;
2831
pub use wait_cloud_init::WaitForCloudInitStep;

src/domain/environment/state/release_failed.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ pub enum ReleaseStep {
5858
CreateBackupStorage,
5959
/// Deploying Backup configuration to the remote host via Ansible (if backup enabled)
6060
DeployBackupConfigToRemote,
61+
/// Installing backup crontab and maintenance script (if backup enabled)
62+
InstallBackupCrontab,
6163
/// Rendering Caddy configuration templates to the build directory (if HTTPS enabled)
6264
RenderCaddyTemplates,
6365
/// Deploying Caddy configuration to the remote host via Ansible (if HTTPS enabled)
@@ -85,6 +87,7 @@ impl fmt::Display for ReleaseStep {
8587
Self::RenderBackupTemplates => "Render Backup Templates",
8688
Self::CreateBackupStorage => "Create Backup Storage",
8789
Self::DeployBackupConfigToRemote => "Deploy Backup Config to Remote",
90+
Self::InstallBackupCrontab => "Install Backup Crontab",
8891
Self::RenderCaddyTemplates => "Render Caddy Templates",
8992
Self::DeployCaddyConfigToRemote => "Deploy Caddy Config to Remote",
9093
Self::RenderDockerComposeTemplates => "Render Docker Compose Templates",

src/infrastructure/templating/ansible/template/renderer/project_generator.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ impl AnsibleProjectGenerator {
316316
"create-mysql-storage.yml",
317317
"create-backup-storage.yml",
318318
"deploy-backup-config.yml",
319+
"install-backup-crontab.yml",
319320
"deploy-caddy-config.yml",
320321
"deploy-compose-files.yml",
321322
"run-compose-services.yml",
@@ -326,7 +327,7 @@ impl AnsibleProjectGenerator {
326327

327328
tracing::debug!(
328329
"Successfully copied {} static template files",
329-
22 // ansible.cfg + 21 playbooks
330+
23 // ansible.cfg + 22 playbooks
330331
);
331332

332333
Ok(())

0 commit comments

Comments
 (0)