Skip to content

Commit 5c36efe

Browse files
committed
refactor: replace anyhow::Result with concrete error enums
- Add ProvisionTemplateError for template rendering operations - Add SshError for connectivity operations - Add RemoteActionError for validation actions - Improve error context with structured variants - Update all method signatures to use concrete errors - Add proper error mapping and source chaining All tests passing (176 tests) and linters clean.
1 parent 6be2d6b commit 5c36efe

File tree

10 files changed

+443
-81
lines changed

10 files changed

+443
-81
lines changed

src/actions/cloud_init.rs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
use anyhow::{anyhow, Context, Result};
21
use std::path::Path;
32
use tracing::info;
43

5-
use crate::actions::RemoteAction;
4+
use crate::actions::{RemoteAction, RemoteActionError};
65
use crate::ssh::SshClient;
76

87
/// Action that checks if cloud-init has completed successfully on the server
@@ -29,30 +28,39 @@ impl RemoteAction for CloudInitValidator {
2928
"cloud-init-validation"
3029
}
3130

32-
async fn execute(&self, server_ip: &str) -> Result<()> {
31+
async fn execute(&self, server_ip: &str) -> Result<(), RemoteActionError> {
3332
info!("🔍 Validating cloud-init completion...");
3433

3534
// Check cloud-init status
3635
let status_output = self
3736
.ssh_client
3837
.execute(server_ip, "cloud-init status")
39-
.context("Failed to check cloud-init status")?;
38+
.map_err(|source| RemoteActionError::SshCommandFailed {
39+
action_name: self.name().to_string(),
40+
source,
41+
})?;
4042

4143
if !status_output.contains("status: done") {
42-
return Err(anyhow!(
43-
"Cloud-init status is not 'done': {}",
44-
status_output
45-
));
44+
return Err(RemoteActionError::ValidationFailed {
45+
action_name: self.name().to_string(),
46+
message: format!("Cloud-init status is not 'done': {status_output}"),
47+
});
4648
}
4749

4850
// Check for completion marker file
4951
let marker_exists = self
5052
.ssh_client
5153
.check_command(server_ip, "test -f /var/lib/cloud/instance/boot-finished")
52-
.context("Failed to check cloud-init completion marker")?;
54+
.map_err(|source| RemoteActionError::SshCommandFailed {
55+
action_name: self.name().to_string(),
56+
source,
57+
})?;
5358

5459
if !marker_exists {
55-
return Err(anyhow!("Cloud-init completion marker file not found"));
60+
return Err(RemoteActionError::ValidationFailed {
61+
action_name: self.name().to_string(),
62+
message: "Cloud-init completion marker file not found".to_string(),
63+
});
5664
}
5765

5866
info!("✅ Cloud-init validation passed");

src/actions/docker.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
use anyhow::{Context, Result};
21
use std::path::Path;
32
use tracing::{info, warn};
43

5-
use crate::actions::RemoteAction;
4+
use crate::actions::{RemoteAction, RemoteActionError};
65
use crate::ssh::SshClient;
76

87
/// Action that validates Docker installation and daemon status on the server
@@ -29,7 +28,7 @@ impl RemoteAction for DockerValidator {
2928
"docker-validation"
3029
}
3130

32-
async fn execute(&self, server_ip: &str) -> Result<()> {
31+
async fn execute(&self, server_ip: &str) -> Result<(), RemoteActionError> {
3332
info!("🔍 Validating Docker installation...");
3433

3534
// Check Docker version
@@ -48,7 +47,10 @@ impl RemoteAction for DockerValidator {
4847
let daemon_active = self
4948
.ssh_client
5049
.check_command(server_ip, "sudo systemctl is-active docker")
51-
.context("Failed to check Docker daemon status")?;
50+
.map_err(|source| RemoteActionError::SshCommandFailed {
51+
action_name: self.name().to_string(),
52+
source,
53+
})?;
5254

5355
if daemon_active {
5456
info!(" ✓ Docker daemon is active");

src/actions/docker_compose.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
use anyhow::{Context, Result};
21
use std::path::Path;
32
use tracing::{info, warn};
43

5-
use crate::actions::RemoteAction;
4+
use crate::actions::{RemoteAction, RemoteActionError};
65
use crate::ssh::SshClient;
76

87
/// Action that validates Docker Compose installation and basic functionality on the server
@@ -29,14 +28,17 @@ impl RemoteAction for DockerComposeValidator {
2928
"docker-compose-validation"
3029
}
3130

32-
async fn execute(&self, server_ip: &str) -> Result<()> {
31+
async fn execute(&self, server_ip: &str) -> Result<(), RemoteActionError> {
3332
info!("🔍 Validating Docker Compose installation...");
3433

3534
// First check if Docker is available (Docker Compose requires Docker)
3635
let docker_available = self
3736
.ssh_client
3837
.check_command(server_ip, "docker --version")
39-
.context("Failed to check Docker availability for Compose")?;
38+
.map_err(|source| RemoteActionError::SshCommandFailed {
39+
action_name: self.name().to_string(),
40+
source,
41+
})?;
4042

4143
if !docker_available {
4244
warn!("⚠️ Docker Compose validation skipped");
@@ -73,7 +75,10 @@ impl RemoteAction for DockerComposeValidator {
7375
server_ip,
7476
&format!("echo '{test_compose_content}' > /tmp/test-docker-compose.yml"),
7577
)
76-
.context("Failed to create test docker-compose.yml")?;
78+
.map_err(|source| RemoteActionError::SshCommandFailed {
79+
action_name: self.name().to_string(),
80+
source,
81+
})?;
7782

7883
if !create_test_success {
7984
warn!(" ⚠️ Could not create test docker-compose.yml file");
@@ -87,7 +92,10 @@ impl RemoteAction for DockerComposeValidator {
8792
server_ip,
8893
"cd /tmp && docker-compose -f test-docker-compose.yml config",
8994
)
90-
.context("Failed to validate docker-compose configuration")?;
95+
.map_err(|source| RemoteActionError::SshCommandFailed {
96+
action_name: self.name().to_string(),
97+
source,
98+
})?;
9199

92100
if validate_success {
93101
info!(" ✓ Docker Compose configuration validation passed");

src/actions/mod.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,32 @@
1-
use anyhow::Result;
1+
use thiserror::Error;
2+
3+
use crate::command::CommandError;
4+
5+
/// Errors that can occur during remote action execution
6+
#[derive(Error, Debug)]
7+
pub enum RemoteActionError {
8+
/// SSH command execution failed
9+
#[error("SSH command execution failed during '{action_name}': {source}")]
10+
SshCommandFailed {
11+
action_name: String,
12+
#[source]
13+
source: CommandError,
14+
},
15+
16+
/// Action validation failed
17+
#[error("Action '{action_name}' validation failed: {message}")]
18+
ValidationFailed {
19+
action_name: String,
20+
message: String,
21+
},
22+
23+
/// Action execution failed with custom error
24+
#[error("Action '{action_name}' execution failed: {message}")]
25+
ExecutionFailed {
26+
action_name: String,
27+
message: String,
28+
},
29+
}
230

331
pub mod cloud_init;
432
pub mod docker;
@@ -29,6 +57,6 @@ pub trait RemoteAction {
2957
///
3058
/// # Returns
3159
/// * `Ok(())` if the action executes successfully
32-
/// * `Err(anyhow::Error)` if the action fails or encounters an error
33-
async fn execute(&self, server_ip: &str) -> Result<()>;
60+
/// * `Err(RemoteActionError)` if the action fails or encounters an error
61+
async fn execute(&self, server_ip: &str) -> Result<(), RemoteActionError>;
3462
}

src/bin/e2e_tests.rs

Lines changed: 26 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use torrust_tracker_deploy::ansible::AnsibleClient;
1111
use torrust_tracker_deploy::lxd::LxdClient;
1212
use torrust_tracker_deploy::opentofu::OpenTofuClient;
1313
use torrust_tracker_deploy::ssh::SshClient;
14+
use torrust_tracker_deploy::stages::ProvisionTemplateRenderer;
1415
// Import template system
1516
use torrust_tracker_deploy::template::file::File;
1617
use torrust_tracker_deploy::template::wrappers::ansible::inventory::{
@@ -49,6 +50,7 @@ struct TestEnvironment {
4950
ssh_key_path: PathBuf,
5051
template_manager: TemplateManager,
5152
opentofu_client: OpenTofuClient,
53+
provision_renderer: ProvisionTemplateRenderer,
5254
ssh_client: SshClient,
5355
lxd_client: LxdClient,
5456
ansible_client: AnsibleClient,
@@ -105,6 +107,10 @@ impl TestEnvironment {
105107
// Create Ansible client pointing to build/ansible directory
106108
let ansible_client = AnsibleClient::new(project_root.join("build/ansible"), verbose);
107109

110+
// Create provision template renderer
111+
let provision_renderer =
112+
ProvisionTemplateRenderer::new(project_root.join("build"), verbose);
113+
108114
if verbose {
109115
println!(
110116
"🔑 SSH key copied to temporary location: {}",
@@ -125,6 +131,7 @@ impl TestEnvironment {
125131
ssh_key_path: temp_ssh_key,
126132
template_manager,
127133
opentofu_client,
134+
provision_renderer,
128135
ssh_client,
129136
lxd_client,
130137
ansible_client,
@@ -135,42 +142,10 @@ impl TestEnvironment {
135142

136143
/// Stage 1: Render provision templates (`OpenTofu`) to build/tofu/ directory
137144
async fn render_provision_templates(&self) -> Result<()> {
138-
println!("🏗️ Stage 1: Rendering provision templates to build directory...");
139-
140-
// Create build directory structure
141-
let build_tofu_dir = self.build_dir.join("tofu/lxd");
142-
tokio::fs::create_dir_all(&build_tofu_dir)
143-
.await
144-
.context("Failed to create build/tofu/lxd directory")?;
145-
146-
// Copy static tofu templates (no variables for now)
147-
// Get template paths, creating them from embedded resources if needed
148-
let source_main_tf = self
149-
.template_manager
150-
.get_template_path("tofu/lxd/main.tf")?;
151-
let dest_main_tf = build_tofu_dir.join("main.tf");
152-
tokio::fs::copy(&source_main_tf, &dest_main_tf)
153-
.await
154-
.context("Failed to copy main.tf to build directory")?;
155-
156-
// Copy cloud-init.yml
157-
let source_cloud_init = self
158-
.template_manager
159-
.get_template_path("tofu/lxd/cloud-init.yml")?;
160-
let dest_cloud_init = build_tofu_dir.join("cloud-init.yml");
161-
tokio::fs::copy(&source_cloud_init, &dest_cloud_init)
145+
self.provision_renderer
146+
.render(&self.template_manager)
162147
.await
163-
.context("Failed to copy cloud-init.yml to build directory")?;
164-
165-
if self.verbose {
166-
println!(
167-
" ✅ Provision templates copied to: {}",
168-
build_tofu_dir.display()
169-
);
170-
}
171-
172-
println!("✅ Stage 1 complete: Provision templates ready");
173-
Ok(())
148+
.map_err(|e| anyhow::anyhow!(e))
174149
}
175150

176151
/// Stage 3: Render configuration templates (`Ansible`) with runtime variables to build/ansible/
@@ -384,7 +359,10 @@ async fn run_full_deployment_test(env: &TestEnvironment) -> Result<()> {
384359
let instance_ip = env.provision_infrastructure()?;
385360

386361
// Wait for SSH connectivity
387-
env.ssh_client.wait_for_connectivity(&instance_ip).await?;
362+
env.ssh_client
363+
.wait_for_connectivity(&instance_ip)
364+
.await
365+
.map_err(|e| anyhow::anyhow!(e))?;
388366

389367
// Stage 3: Render configuration templates with runtime variables
390368
env.render_configuration_templates(&instance_ip).await?;
@@ -395,7 +373,10 @@ async fn run_full_deployment_test(env: &TestEnvironment) -> Result<()> {
395373

396374
// Validate cloud-init completion
397375
let cloud_init_validator = CloudInitValidator::new(&env.ssh_key_path, "torrust", env.verbose);
398-
cloud_init_validator.execute(&instance_ip).await?;
376+
cloud_init_validator
377+
.execute(&instance_ip)
378+
.await
379+
.map_err(|e| anyhow::anyhow!(e))?;
399380

400381
// Run the install-docker playbook
401382
// NOTE: We skip the update-apt-cache playbook in E2E tests to avoid CI network issues
@@ -405,7 +386,10 @@ async fn run_full_deployment_test(env: &TestEnvironment) -> Result<()> {
405386

406387
// 7. Validate Docker installation
407388
let docker_validator = DockerValidator::new(&env.ssh_key_path, "torrust", env.verbose);
408-
docker_validator.execute(&instance_ip).await?;
389+
docker_validator
390+
.execute(&instance_ip)
391+
.await
392+
.map_err(|e| anyhow::anyhow!(e))?;
409393

410394
// 8. Run the install-docker-compose playbook
411395
println!("📋 Step 3: Installing Docker Compose...");
@@ -414,7 +398,10 @@ async fn run_full_deployment_test(env: &TestEnvironment) -> Result<()> {
414398
// 9. Validate Docker Compose installation
415399
let docker_compose_validator =
416400
DockerComposeValidator::new(&env.ssh_key_path, "torrust", env.verbose);
417-
docker_compose_validator.execute(&instance_ip).await?;
401+
docker_compose_validator
402+
.execute(&instance_ip)
403+
.await
404+
.map_err(|e| anyhow::anyhow!(e))?;
418405

419406
println!("🎉 Full deployment E2E test completed successfully!");
420407
println!(" ✅ Cloud-init setup completed");

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ pub mod command;
2525
pub mod lxd;
2626
pub mod opentofu;
2727
pub mod ssh;
28+
pub mod stages;
2829
pub mod template;

0 commit comments

Comments
 (0)