Skip to content

Commit fd82fde

Browse files
committed
refactor: replace anyhow errors with explicit ProvisionedContainerError enum
- Replace generic anyhow::Error with specific ProvisionedContainerError enum in provisioned_container.rs - Add 6 error variants with proper error chaining using thiserror - Update setup_ssh_keys method to accept SshCredentials parameter instead of hardcoded paths - Integrate e2e_config_tests.rs with TestEnvironment SSH credential management - Add comprehensive unit tests for error handling and error chain preservation - Improve error context with specific messages for each failure mode
1 parent 2625b5f commit fd82fde

File tree

2 files changed

+168
-25
lines changed

2 files changed

+168
-25
lines changed

src/bin/e2e_config_tests.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,13 @@ fn run_configuration_tests() -> Result<()> {
128128
info!("Running preflight cleanup for Docker-based E2E tests");
129129
let instance_name = InstanceName::new("torrust-tracker-vm".to_string())
130130
.context("Failed to create instance name")?;
131-
let test_env = TestEnvironment::new(false, "./data/templates", instance_name)
132-
.context("Failed to create test environment")?;
131+
let test_env = TestEnvironment::with_ssh_user_and_init(
132+
false,
133+
"./data/templates",
134+
"torrust",
135+
instance_name,
136+
)
137+
.context("Failed to create test environment")?;
133138

134139
preflight_cleanup::cleanup_lingering_resources_docker(&test_env)
135140
.context("Failed to complete preflight cleanup")?;
@@ -145,14 +150,17 @@ fn run_configuration_tests() -> Result<()> {
145150
.wait_for_ssh()
146151
.context("SSH server failed to start")?;
147152

153+
// Get SSH credentials from test environment and setup keys
154+
let ssh_credentials = &test_env.config.ssh_credentials;
148155
running_container
149-
.setup_ssh_keys()
156+
.setup_ssh_keys(ssh_credentials)
150157
.context("Failed to setup SSH authentication")?;
151158

152159
let (ssh_host, ssh_port) = running_container.ssh_details();
153160
info!(
154161
ssh_host = %ssh_host,
155162
ssh_port = ssh_port,
163+
ssh_user = %ssh_credentials.ssh_username,
156164
container_id = %running_container.container_id(),
157165
"Container ready for Ansible configuration"
158166
);

src/e2e/provisioned_container.rs

Lines changed: 157 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,39 @@
1010
//! - `RunningProvisionedContainer` - Running state, can be queried, configured, and stopped
1111
//! - State transitions are enforced at compile time through different types
1212
//!
13+
//! ## Error Handling
14+
//!
15+
//! This module uses explicit error types through [`ProvisionedContainerError`] instead of
16+
//! generic `anyhow` errors. Each error variant provides specific information about what
17+
//! went wrong, making it easier to handle different failure modes appropriately.
18+
//!
1319
//! ## Usage
1420
//!
1521
//! ```rust,no_run
16-
//! use anyhow::Result;
17-
//! use torrust_tracker_deploy::e2e::provisioned_container::StoppedProvisionedContainer;
22+
//! use torrust_tracker_deploy::e2e::provisioned_container::{
23+
//! StoppedProvisionedContainer, ProvisionedContainerError
24+
//! };
25+
//! use torrust_tracker_deploy::infrastructure::adapters::ssh::SshCredentials;
26+
//! use std::path::PathBuf;
1827
//!
19-
//! fn example() -> Result<()> {
28+
//! fn example() -> Result<(), ProvisionedContainerError> {
2029
//! // Start with stopped state
2130
//! let stopped = StoppedProvisionedContainer::default();
2231
//!
2332
//! // Transition to running state
2433
//! let running = stopped.start()?;
2534
//!
26-
//! // Operations only available when running
35+
//! // Wait for SSH server
2736
//! running.wait_for_ssh()?;
28-
//! running.setup_ssh_keys()?;
37+
//!
38+
//! // Setup SSH keys with credentials
39+
//! let ssh_credentials = SshCredentials::new(
40+
//! PathBuf::from("/path/to/private_key"),
41+
//! PathBuf::from("/path/to/public_key.pub"),
42+
//! "torrust".to_string(),
43+
//! );
44+
//! running.setup_ssh_keys(&ssh_credentials)?;
45+
//!
2946
//! let (host, port) = running.ssh_details();
3047
//!
3148
//! // Transition back to stopped state
@@ -34,7 +51,6 @@
3451
//! }
3552
//! ```
3653
37-
use anyhow::{Context, Result};
3854
use std::time::Duration;
3955
use testcontainers::{
4056
core::{IntoContainerPort, WaitFor},
@@ -43,6 +59,55 @@ use testcontainers::{
4359
};
4460
use tracing::info;
4561

62+
use crate::infrastructure::adapters::ssh::SshCredentials;
63+
64+
/// Specific error types for provisioned container operations
65+
#[derive(Debug, thiserror::Error)]
66+
pub enum ProvisionedContainerError {
67+
/// Docker build command execution failed
68+
#[error("Failed to execute docker build command: {source}")]
69+
DockerBuildExecution {
70+
#[source]
71+
source: std::io::Error,
72+
},
73+
74+
/// Docker build process failed with non-zero exit code
75+
#[error("Docker build failed with stderr: {stderr}")]
76+
DockerBuildFailed { stderr: String },
77+
78+
/// Container failed to start
79+
#[error("Failed to start container: {source}")]
80+
ContainerStartFailed {
81+
#[source]
82+
source: testcontainers::TestcontainersError,
83+
},
84+
85+
/// Failed to get mapped SSH port from container
86+
#[error("Failed to get mapped SSH port: {source}")]
87+
SshPortMappingFailed {
88+
#[source]
89+
source: testcontainers::TestcontainersError,
90+
},
91+
92+
/// Failed to read SSH public key file
93+
#[error("Failed to read SSH public key from {path}: {source}")]
94+
SshKeyFileRead {
95+
path: String,
96+
#[source]
97+
source: std::io::Error,
98+
},
99+
100+
/// Failed to execute SSH key setup command in container
101+
#[error("Failed to setup SSH keys in container: {source}")]
102+
SshKeySetupFailed {
103+
#[source]
104+
source: testcontainers::TestcontainersError,
105+
},
106+
}
107+
108+
/// Result type alias for provisioned container operations
109+
pub type Result<T> = std::result::Result<T, ProvisionedContainerError>;
110+
46111
/// Container configuration following state machine pattern
47112
///
48113
/// Following the pattern from Torrust Tracker `MySQL` driver, where different states
@@ -66,11 +131,13 @@ impl StoppedProvisionedContainer {
66131
".",
67132
])
68133
.output()
69-
.context("Failed to execute docker build command")?;
134+
.map_err(|source| ProvisionedContainerError::DockerBuildExecution { source })?;
70135

71136
if !output.status.success() {
72137
let stderr = String::from_utf8_lossy(&output.stderr);
73-
return Err(anyhow::anyhow!("Docker build failed: {stderr}"));
138+
return Err(ProvisionedContainerError::DockerBuildFailed {
139+
stderr: stderr.to_string(),
140+
});
74141
}
75142

76143
info!("Docker image built successfully");
@@ -96,12 +163,14 @@ impl StoppedProvisionedContainer {
96163
.with_exposed_port(22.tcp())
97164
.with_wait_for(WaitFor::message_on_stdout("sshd entered RUNNING state"));
98165

99-
let container = image.start().context("Failed to start container")?;
166+
let container = image
167+
.start()
168+
.map_err(|source| ProvisionedContainerError::ContainerStartFailed { source })?;
100169

101170
// Get the actual mapped port from testcontainers
102171
let ssh_port = container
103172
.get_host_port_ipv4(22.tcp())
104-
.context("Failed to get mapped SSH port")?;
173+
.map_err(|source| ProvisionedContainerError::SshPortMappingFailed { source })?;
105174

106175
info!(
107176
container_id = %container.id(),
@@ -146,40 +215,61 @@ impl RunningProvisionedContainer {
146215
std::thread::sleep(Duration::from_secs(5));
147216

148217
info!("SSH server should be ready");
218+
149219
Ok(())
150220
}
151221

152222
/// Setup SSH key authentication (only available when running)
153223
///
224+
/// # Arguments
225+
///
226+
/// * `ssh_credentials` - SSH credentials containing the public key path and username
227+
///
154228
/// # Errors
155229
///
156230
/// Returns an error if:
231+
/// - SSH public key file cannot be read
157232
/// - Docker exec command fails
158233
/// - SSH key file operations fail within the container
159-
pub fn setup_ssh_keys(&self) -> Result<()> {
234+
pub fn setup_ssh_keys(&self, ssh_credentials: &SshCredentials) -> Result<()> {
160235
info!("Setting up SSH key authentication");
161236

162-
// Read the public key from fixtures
163-
let project_root = std::env::current_dir().context("Failed to get current directory")?;
164-
let public_key_path = project_root.join("fixtures/testing_rsa.pub");
165-
let public_key_content =
166-
std::fs::read_to_string(&public_key_path).context("Failed to read SSH public key")?;
237+
// Read the public key from the credentials
238+
let public_key_content = std::fs::read_to_string(&ssh_credentials.ssh_pub_key_path)
239+
.map_err(|source| ProvisionedContainerError::SshKeyFileRead {
240+
path: ssh_credentials.ssh_pub_key_path.display().to_string(),
241+
source,
242+
})?;
167243

168-
// Copy the public key into the container's authorized_keys
244+
// Create the authorized_keys file for the SSH user in the container
245+
let ssh_user = &ssh_credentials.ssh_username;
246+
let user_ssh_dir = format!("/home/{ssh_user}/.ssh");
247+
let authorized_keys_path = format!("{user_ssh_dir}/authorized_keys");
248+
249+
// Copy the public key into the container's authorized_keys for the specified user
169250
let exec_result = self.container.exec(testcontainers::core::ExecCommand::new([
170251
"sh",
171252
"-c",
172-
&format!("echo '{}' >> /home/torrust/.ssh/authorized_keys && chmod 600 /home/torrust/.ssh/authorized_keys", public_key_content.trim()),
253+
&format!(
254+
"mkdir -p {} && echo '{}' >> {} && chmod 700 {} && chmod 600 {}",
255+
user_ssh_dir,
256+
public_key_content.trim(),
257+
authorized_keys_path,
258+
user_ssh_dir,
259+
authorized_keys_path
260+
),
173261
]));
174262

175263
match exec_result {
176264
Ok(_) => {
177-
info!("SSH key authentication configured");
265+
info!(
266+
ssh_user = ssh_user,
267+
authorized_keys = authorized_keys_path,
268+
"SSH key authentication configured"
269+
);
178270
Ok(())
179271
}
180-
Err(e) => Err(anyhow::anyhow!(
181-
"Failed to setup SSH keys in container: {e}"
182-
)),
272+
Err(source) => Err(ProvisionedContainerError::SshKeySetupFailed { source }),
183273
}
184274
}
185275

@@ -200,6 +290,8 @@ impl RunningProvisionedContainer {
200290
#[cfg(test)]
201291
mod tests {
202292
use super::*;
293+
use std::error::Error;
294+
use std::path::PathBuf;
203295

204296
#[test]
205297
fn it_should_create_default_stopped_container() {
@@ -210,6 +302,49 @@ mod tests {
210302
)); // Just test it exists
211303
}
212304

305+
#[test]
306+
fn it_should_have_proper_error_display_messages() {
307+
let error = ProvisionedContainerError::DockerBuildFailed {
308+
stderr: "test error message".to_string(),
309+
};
310+
assert!(error.to_string().contains("Docker build failed"));
311+
assert!(error.to_string().contains("test error message"));
312+
}
313+
314+
#[test]
315+
fn it_should_preserve_error_chain_for_docker_build_execution() {
316+
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "docker not found");
317+
let error = ProvisionedContainerError::DockerBuildExecution { source: io_error };
318+
319+
assert!(error
320+
.to_string()
321+
.contains("Failed to execute docker build command"));
322+
assert!(error.source().is_some());
323+
}
324+
325+
#[test]
326+
fn it_should_preserve_error_chain_for_ssh_key_file_read() {
327+
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
328+
let error = ProvisionedContainerError::SshKeyFileRead {
329+
path: "/path/to/key".to_string(),
330+
source: io_error,
331+
};
332+
333+
assert!(error.to_string().contains("Failed to read SSH public key"));
334+
assert!(error.to_string().contains("/path/to/key"));
335+
assert!(error.source().is_some());
336+
}
337+
213338
// Note: Integration tests that actually start containers would require Docker
214339
// and are better suited for the e2e test binaries
340+
341+
// Helper function to create mock SSH credentials for testing
342+
#[allow(dead_code)]
343+
fn create_mock_ssh_credentials() -> SshCredentials {
344+
SshCredentials::new(
345+
PathBuf::from("/mock/path/to/private_key"),
346+
PathBuf::from("/mock/path/to/public_key.pub"),
347+
"testuser".to_string(),
348+
)
349+
}
215350
}

0 commit comments

Comments
 (0)