10
10
//! - `RunningProvisionedContainer` - Running state, can be queried, configured, and stopped
11
11
//! - State transitions are enforced at compile time through different types
12
12
//!
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
+ //!
13
19
//! ## Usage
14
20
//!
15
21
//! ```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;
18
27
//!
19
- //! fn example() -> Result<()> {
28
+ //! fn example() -> Result<(), ProvisionedContainerError > {
20
29
//! // Start with stopped state
21
30
//! let stopped = StoppedProvisionedContainer::default();
22
31
//!
23
32
//! // Transition to running state
24
33
//! let running = stopped.start()?;
25
34
//!
26
- //! // Operations only available when running
35
+ //! // Wait for SSH server
27
36
//! 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
+ //!
29
46
//! let (host, port) = running.ssh_details();
30
47
//!
31
48
//! // Transition back to stopped state
34
51
//! }
35
52
//! ```
36
53
37
- use anyhow:: { Context , Result } ;
38
54
use std:: time:: Duration ;
39
55
use testcontainers:: {
40
56
core:: { IntoContainerPort , WaitFor } ,
@@ -43,6 +59,55 @@ use testcontainers::{
43
59
} ;
44
60
use tracing:: info;
45
61
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
+
46
111
/// Container configuration following state machine pattern
47
112
///
48
113
/// Following the pattern from Torrust Tracker `MySQL` driver, where different states
@@ -66,11 +131,13 @@ impl StoppedProvisionedContainer {
66
131
"." ,
67
132
] )
68
133
. output ( )
69
- . context ( "Failed to execute docker build command" ) ?;
134
+ . map_err ( |source| ProvisionedContainerError :: DockerBuildExecution { source } ) ?;
70
135
71
136
if !output. status . success ( ) {
72
137
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
+ } ) ;
74
141
}
75
142
76
143
info ! ( "Docker image built successfully" ) ;
@@ -96,12 +163,14 @@ impl StoppedProvisionedContainer {
96
163
. with_exposed_port ( 22 . tcp ( ) )
97
164
. with_wait_for ( WaitFor :: message_on_stdout ( "sshd entered RUNNING state" ) ) ;
98
165
99
- let container = image. start ( ) . context ( "Failed to start container" ) ?;
166
+ let container = image
167
+ . start ( )
168
+ . map_err ( |source| ProvisionedContainerError :: ContainerStartFailed { source } ) ?;
100
169
101
170
// Get the actual mapped port from testcontainers
102
171
let ssh_port = container
103
172
. get_host_port_ipv4 ( 22 . tcp ( ) )
104
- . context ( "Failed to get mapped SSH port" ) ?;
173
+ . map_err ( |source| ProvisionedContainerError :: SshPortMappingFailed { source } ) ?;
105
174
106
175
info ! (
107
176
container_id = %container. id( ) ,
@@ -146,40 +215,61 @@ impl RunningProvisionedContainer {
146
215
std:: thread:: sleep ( Duration :: from_secs ( 5 ) ) ;
147
216
148
217
info ! ( "SSH server should be ready" ) ;
218
+
149
219
Ok ( ( ) )
150
220
}
151
221
152
222
/// Setup SSH key authentication (only available when running)
153
223
///
224
+ /// # Arguments
225
+ ///
226
+ /// * `ssh_credentials` - SSH credentials containing the public key path and username
227
+ ///
154
228
/// # Errors
155
229
///
156
230
/// Returns an error if:
231
+ /// - SSH public key file cannot be read
157
232
/// - Docker exec command fails
158
233
/// - 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 < ( ) > {
160
235
info ! ( "Setting up SSH key authentication" ) ;
161
236
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
+ } ) ?;
167
243
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
169
250
let exec_result = self . container . exec ( testcontainers:: core:: ExecCommand :: new ( [
170
251
"sh" ,
171
252
"-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
+ ) ,
173
261
] ) ) ;
174
262
175
263
match exec_result {
176
264
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
+ ) ;
178
270
Ok ( ( ) )
179
271
}
180
- Err ( e) => Err ( anyhow:: anyhow!(
181
- "Failed to setup SSH keys in container: {e}"
182
- ) ) ,
272
+ Err ( source) => Err ( ProvisionedContainerError :: SshKeySetupFailed { source } ) ,
183
273
}
184
274
}
185
275
@@ -200,6 +290,8 @@ impl RunningProvisionedContainer {
200
290
#[ cfg( test) ]
201
291
mod tests {
202
292
use super :: * ;
293
+ use std:: error:: Error ;
294
+ use std:: path:: PathBuf ;
203
295
204
296
#[ test]
205
297
fn it_should_create_default_stopped_container ( ) {
@@ -210,6 +302,49 @@ mod tests {
210
302
) ) ; // Just test it exists
211
303
}
212
304
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
+
213
338
// Note: Integration tests that actually start containers would require Docker
214
339
// 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
+ }
215
350
}
0 commit comments