Skip to content

Commit a8385c6

Browse files
committed
refactor: split SshConfig into SshCredentials and SshConnection
Splits SSH configuration into two focused types: - SshCredentials: Static authentication info (username, key paths) - SshConnection: Complete connection info (credentials + host IP) This eliminates the need for placeholder IP addresses like '0.0.0.0' and provides better type safety by separating static configuration from runtime connection state. Changes: - Created SshCredentials with promotion method with_host() - Created SshConnection with accessor methods - Updated all usages in actions, steps, and command wrappers - Removed original SshConfig struct - Updated module exports and documentation
1 parent aa8909b commit a8385c6

File tree

8 files changed

+175
-129
lines changed

8 files changed

+175
-129
lines changed

src/actions/cloud_init.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use tracing::info;
33

44
use crate::actions::{RemoteAction, RemoteActionError};
55
use crate::command_wrappers::ssh::SshClient;
6-
use crate::config::ssh::SshConfig;
6+
use crate::config::ssh::SshConnection;
77

88
/// Action that checks if cloud-init has completed successfully on the server
99
pub struct CloudInitValidator {
@@ -14,10 +14,10 @@ impl CloudInitValidator {
1414
/// Create a new `CloudInitValidator` with the specified SSH configuration
1515
///
1616
/// # Arguments
17-
/// * `ssh_config` - SSH configuration containing key path, username, host IP, etc.
17+
/// * `ssh_connection` - SSH connection configuration containing credentials and host IP
1818
#[must_use]
19-
pub fn new(ssh_config: SshConfig) -> Self {
20-
let ssh_client = SshClient::new(ssh_config);
19+
pub fn new(ssh_connection: SshConnection) -> Self {
20+
let ssh_client = SshClient::new(ssh_connection);
2121
Self { ssh_client }
2222
}
2323
}

src/actions/docker.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use tracing::{info, warn};
33

44
use crate::actions::{RemoteAction, RemoteActionError};
55
use crate::command_wrappers::ssh::SshClient;
6-
use crate::config::ssh::SshConfig;
6+
use crate::config::ssh::SshConnection;
77

88
/// Action that validates Docker installation and daemon status on the server
99
pub struct DockerValidator {
@@ -14,10 +14,10 @@ impl DockerValidator {
1414
/// Create a new `DockerValidator` with the specified SSH configuration
1515
///
1616
/// # Arguments
17-
/// * `ssh_config` - SSH configuration containing key path, username, host IP, etc.
17+
/// * `ssh_connection` - SSH connection configuration containing credentials and host IP
1818
#[must_use]
19-
pub fn new(ssh_config: SshConfig) -> Self {
20-
let ssh_client = SshClient::new(ssh_config);
19+
pub fn new(ssh_connection: SshConnection) -> Self {
20+
let ssh_client = SshClient::new(ssh_connection);
2121
Self { ssh_client }
2222
}
2323
}

src/actions/docker_compose.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use tracing::{info, warn};
33

44
use crate::actions::{RemoteAction, RemoteActionError};
55
use crate::command_wrappers::ssh::SshClient;
6-
use crate::config::ssh::SshConfig;
6+
use crate::config::ssh::SshConnection;
77

88
/// Action that validates Docker Compose installation and basic functionality on the server
99
pub struct DockerComposeValidator {
@@ -14,10 +14,10 @@ impl DockerComposeValidator {
1414
/// Create a new `DockerComposeValidator` with the specified SSH configuration
1515
///
1616
/// # Arguments
17-
/// * `ssh_config` - SSH configuration containing key path, username, host IP, etc.
17+
/// * `ssh_connection` - SSH connection configuration containing credentials and host IP
1818
#[must_use]
19-
pub fn new(ssh_config: SshConfig) -> Self {
20-
let ssh_client = SshClient::new(ssh_config);
19+
pub fn new(ssh_connection: SshConnection) -> Self {
20+
let ssh_client = SshClient::new(ssh_connection);
2121
Self { ssh_client }
2222
}
2323
}

src/bin/e2e_tests.rs

Lines changed: 15 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use tracing::{error, info, warn};
88
use tracing_subscriber::EnvFilter;
99

1010
// Import command execution system
11-
use torrust_tracker_deploy::config::{Config, SshConfig};
11+
use torrust_tracker_deploy::config::{Config, SshCredentials};
1212
use torrust_tracker_deploy::container::Services;
1313
// Import remote actions
1414
use torrust_tracker_deploy::actions::{
@@ -56,18 +56,14 @@ impl TestEnvironment {
5656
let temp_ssh_pub_key = temp_dir.path().join("testing_rsa.pub");
5757
Self::setup_ssh_key(&project_root, &temp_dir, &temp_ssh_key)?;
5858

59-
// Create configuration
60-
// Note: Using placeholder IP since this config is used as a template - actual host IP will be set per connection
61-
let placeholder_ip = "0.0.0.0".parse().expect("Valid IP address");
62-
let ssh_config = SshConfig::new(
63-
temp_ssh_key,
64-
temp_ssh_pub_key,
65-
"torrust".to_string(),
66-
placeholder_ip,
67-
);
59+
// Create SSH credentials (no host IP needed yet)
60+
let ssh_credentials =
61+
SshCredentials::new(temp_ssh_key, temp_ssh_pub_key, "torrust".to_string());
62+
63+
// Create main configuration
6864
let config = Config::new(
6965
keep_env,
70-
ssh_config,
66+
ssh_credentials,
7167
templates_dir,
7268
project_root.clone(),
7369
project_root.join("build"),
@@ -292,13 +288,8 @@ async fn validate_deployment(env: &TestEnvironment, instance_ip: &IpAddr) -> Res
292288
component = "cloud_init",
293289
"Validating cloud-init completion"
294290
);
295-
let cloud_init_ssh_config = SshConfig::new(
296-
env.config.ssh_config.ssh_priv_key_path.clone(),
297-
env.config.ssh_config.ssh_pub_key_path.clone(),
298-
env.config.ssh_config.ssh_username.clone(),
299-
*instance_ip,
300-
);
301-
let cloud_init_validator = CloudInitValidator::new(cloud_init_ssh_config);
291+
let cloud_init_ssh_connection = env.config.ssh_config.clone().with_host(*instance_ip);
292+
let cloud_init_validator = CloudInitValidator::new(cloud_init_ssh_connection);
302293
cloud_init_validator
303294
.execute(instance_ip)
304295
.await
@@ -310,13 +301,8 @@ async fn validate_deployment(env: &TestEnvironment, instance_ip: &IpAddr) -> Res
310301
component = "docker",
311302
"Validating Docker installation"
312303
);
313-
let docker_ssh_config = SshConfig::new(
314-
env.config.ssh_config.ssh_priv_key_path.clone(),
315-
env.config.ssh_config.ssh_pub_key_path.clone(),
316-
env.config.ssh_config.ssh_username.clone(),
317-
*instance_ip,
318-
);
319-
let docker_validator = DockerValidator::new(docker_ssh_config);
304+
let docker_ssh_connection = env.config.ssh_config.clone().with_host(*instance_ip);
305+
let docker_validator = DockerValidator::new(docker_ssh_connection);
320306
docker_validator
321307
.execute(instance_ip)
322308
.await
@@ -328,13 +314,8 @@ async fn validate_deployment(env: &TestEnvironment, instance_ip: &IpAddr) -> Res
328314
component = "docker_compose",
329315
"Validating Docker Compose installation"
330316
);
331-
let docker_compose_ssh_config = SshConfig::new(
332-
env.config.ssh_config.ssh_priv_key_path.clone(),
333-
env.config.ssh_config.ssh_pub_key_path.clone(),
334-
env.config.ssh_config.ssh_username.clone(),
335-
*instance_ip,
336-
);
337-
let docker_compose_validator = DockerComposeValidator::new(docker_compose_ssh_config);
317+
let docker_compose_ssh_connection = env.config.ssh_config.clone().with_host(*instance_ip);
318+
let docker_compose_validator = DockerComposeValidator::new(docker_compose_ssh_connection);
338319
docker_compose_validator
339320
.execute(instance_ip)
340321
.await
@@ -363,13 +344,8 @@ async fn run_full_deployment_test(env: &TestEnvironment) -> Result<IpAddr> {
363344
let instance_ip = env.provision_infrastructure()?;
364345

365346
// Wait for SSH connectivity
366-
let wait_ssh_config = SshConfig::new(
367-
env.config.ssh_config.ssh_priv_key_path.clone(),
368-
env.config.ssh_config.ssh_pub_key_path.clone(),
369-
env.config.ssh_config.ssh_username.clone(),
370-
instance_ip,
371-
);
372-
let wait_ssh_step = WaitForSSHConnectivityStep::new(wait_ssh_config);
347+
let wait_ssh_connection = env.config.ssh_config.clone().with_host(instance_ip);
348+
let wait_ssh_step = WaitForSSHConnectivityStep::new(wait_ssh_connection);
373349
wait_ssh_step
374350
.execute()
375351
.await

src/command_wrappers/ssh.rs

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use thiserror::Error;
44
use tracing::info;
55

66
use crate::command::{CommandError, CommandExecutor};
7-
use crate::config::ssh::SshConfig;
7+
use crate::config::ssh::SshConnection;
88

99
/// Errors that can occur during SSH operations
1010
#[derive(Error, Debug)]
@@ -35,19 +35,19 @@ pub enum SshError {
3535
///
3636
/// Uses `CommandExecutor` as a collaborator for actual command execution.
3737
pub struct SshClient {
38-
ssh_config: SshConfig,
38+
ssh_connection: SshConnection,
3939
command_executor: CommandExecutor,
4040
}
4141

4242
impl SshClient {
4343
/// Creates a new `SshClient`
4444
///
4545
/// # Arguments
46-
/// * `ssh_config` - SSH configuration containing key path, username, host IP, etc.
46+
/// * `ssh_connection` - SSH connection configuration containing credentials and host IP
4747
#[must_use]
48-
pub fn new(ssh_config: SshConfig) -> Self {
48+
pub fn new(ssh_connection: SshConnection) -> Self {
4949
Self {
50-
ssh_config,
50+
ssh_connection,
5151
command_executor: CommandExecutor::new(),
5252
}
5353
}
@@ -56,8 +56,8 @@ impl SshClient {
5656
fn build_ssh_args(&self, remote_command: &str, additional_options: &[&str]) -> Vec<String> {
5757
let mut args = vec![
5858
"-i".to_string(),
59-
self.ssh_config
60-
.ssh_priv_key_path
59+
self.ssh_connection
60+
.ssh_priv_key_path()
6161
.to_string_lossy()
6262
.to_string(),
6363
"-o".to_string(),
@@ -74,7 +74,8 @@ impl SshClient {
7474

7575
args.push(format!(
7676
"{}@{}",
77-
self.ssh_config.ssh_username, self.ssh_config.host_ip
77+
self.ssh_connection.ssh_username(),
78+
self.ssh_connection.host_ip
7879
));
7980
args.push(remote_command.to_string());
8081

@@ -193,7 +194,7 @@ impl SshClient {
193194
pub async fn wait_for_connectivity(&self) -> Result<(), SshError> {
194195
info!(
195196
operation = "ssh_connectivity",
196-
host_ip = %self.ssh_config.host_ip,
197+
host_ip = %self.ssh_connection.host_ip,
197198
"Waiting for SSH connectivity"
198199
);
199200

@@ -208,7 +209,7 @@ impl SshClient {
208209
Ok(true) => {
209210
info!(
210211
operation = "ssh_connectivity",
211-
host_ip = %self.ssh_config.host_ip,
212+
host_ip = %self.ssh_connection.host_ip,
212213
status = "success",
213214
"SSH connectivity established"
214215
);
@@ -219,7 +220,7 @@ impl SshClient {
219220
if (attempt + 1) % 5 == 0 {
220221
info!(
221222
operation = "ssh_connectivity",
222-
host_ip = %self.ssh_config.host_ip,
223+
host_ip = %self.ssh_connection.host_ip,
223224
attempt = attempt + 1,
224225
max_attempts = max_attempts,
225226
"Still waiting for SSH connectivity"
@@ -236,7 +237,7 @@ impl SshClient {
236237
}
237238

238239
Err(SshError::ConnectivityTimeout {
239-
host_ip: self.ssh_config.host_ip.to_string(),
240+
host_ip: self.ssh_connection.host_ip.to_string(),
240241
attempts: max_attempts,
241242
timeout_seconds,
242243
})
@@ -246,46 +247,53 @@ impl SshClient {
246247
#[cfg(test)]
247248
mod tests {
248249
use super::*;
250+
use crate::config::ssh::SshCredentials;
249251
use std::net::{IpAddr, Ipv4Addr};
250252
use std::path::PathBuf;
251253

252254
#[test]
253255
fn it_should_create_ssh_client_with_valid_parameters() {
254256
let host_ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
255-
let ssh_config = SshConfig::new(
257+
let credentials = SshCredentials::new(
256258
PathBuf::from("/path/to/key"),
257259
PathBuf::from("/path/to/key.pub"),
258260
"testuser".to_string(),
259-
host_ip,
260261
);
261-
let ssh_client = SshClient::new(ssh_config);
262+
let ssh_connection = credentials.with_host(host_ip);
263+
let ssh_client = SshClient::new(ssh_connection);
262264

263265
assert_eq!(
264-
ssh_client.ssh_config.ssh_priv_key_path.to_string_lossy(),
266+
ssh_client
267+
.ssh_connection
268+
.ssh_priv_key_path()
269+
.to_string_lossy(),
265270
"/path/to/key"
266271
);
267-
assert_eq!(ssh_client.ssh_config.ssh_username, "testuser");
268-
assert_eq!(ssh_client.ssh_config.host_ip, host_ip);
272+
assert_eq!(ssh_client.ssh_connection.ssh_username(), "testuser");
273+
assert_eq!(ssh_client.ssh_connection.host_ip, host_ip);
269274
// Note: verbose is now encapsulated in the CommandExecutor collaborator
270275
}
271276

272277
#[test]
273278
fn it_should_create_ssh_client_with_connection_details() {
274279
let host_ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
275-
let ssh_config = SshConfig::new(
280+
let credentials = SshCredentials::new(
276281
PathBuf::from("/path/to/key"),
277282
PathBuf::from("/path/to/key.pub"),
278283
"testuser".to_string(),
279-
host_ip,
280284
);
281-
let ssh_client = SshClient::new(ssh_config);
285+
let ssh_connection = credentials.with_host(host_ip);
286+
let ssh_client = SshClient::new(ssh_connection);
282287

283288
assert_eq!(
284-
ssh_client.ssh_config.ssh_priv_key_path.to_string_lossy(),
289+
ssh_client
290+
.ssh_connection
291+
.ssh_priv_key_path()
292+
.to_string_lossy(),
285293
"/path/to/key"
286294
);
287-
assert_eq!(ssh_client.ssh_config.ssh_username, "testuser");
288-
assert_eq!(ssh_client.ssh_config.host_ip, host_ip);
295+
assert_eq!(ssh_client.ssh_connection.ssh_username(), "testuser");
296+
assert_eq!(ssh_client.ssh_connection.host_ip, host_ip);
289297
// Note: logging is now handled by the tracing crate via CommandExecutor
290298
}
291299
}

src/config/mod.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::path::PathBuf;
22

33
pub mod ssh;
4-
pub use ssh::SshConfig;
4+
pub use ssh::{SshConnection, SshCredentials};
55

66
/// Configuration parameters for deployment environments.
77
///
@@ -17,11 +17,12 @@ pub struct Config {
1717
/// will be left running for manual inspection or reuse.
1818
pub keep_env: bool,
1919

20-
/// SSH configuration for remote connections.
20+
/// SSH credentials for remote connections.
2121
///
22-
/// Contains SSH key path and username settings for connecting to
23-
/// deployed instances during the deployment process.
24-
pub ssh_config: SshConfig,
22+
/// Contains SSH key paths and username settings that will be used
23+
/// when connecting to deployed instances. The host IP will be determined
24+
/// later when instances are provisioned.
25+
pub ssh_config: SshCredentials,
2526

2627
/// Subdirectory name for Ansible-related files within the build directory.
2728
///
@@ -72,16 +73,15 @@ impl Config {
7273
/// ```rust
7374
/// # use std::net::{IpAddr, Ipv4Addr};
7475
/// # use std::path::PathBuf;
75-
/// # use torrust_tracker_deploy::config::{Config, SshConfig};
76-
/// let ssh_config = SshConfig::new(
76+
/// # use torrust_tracker_deploy::config::{Config, SshCredentials};
77+
/// let ssh_credentials = SshCredentials::new(
7778
/// PathBuf::from("/home/user/.ssh/deploy_key"),
7879
/// PathBuf::from("/home/user/.ssh/deploy_key.pub"),
7980
/// "ubuntu".to_string(),
80-
/// IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
8181
/// );
8282
/// let config = Config::new(
8383
/// true, // keep environment for debugging
84-
/// ssh_config,
84+
/// ssh_credentials,
8585
/// "templates".to_string(),
8686
/// PathBuf::from("/path/to/project"),
8787
/// PathBuf::from("/path/to/project/build"),
@@ -90,7 +90,7 @@ impl Config {
9090
#[must_use]
9191
pub fn new(
9292
keep_env: bool,
93-
ssh_config: SshConfig,
93+
ssh_config: SshCredentials,
9494
templates_dir: String,
9595
project_root: PathBuf,
9696
build_dir: PathBuf,

0 commit comments

Comments
 (0)