Skip to content

Commit a39ba13

Browse files
committed
feat: [#220] enhance test command with external health checks
1 parent 8e0e096 commit a39ba13

File tree

3 files changed

+121
-46
lines changed

3 files changed

+121
-46
lines changed

src/application/command_handlers/test/errors.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ pub enum TestCommandHandlerError {
2020
#[error("Environment '{environment_name}' does not have an instance IP set. The environment must be provisioned before running tests.")]
2121
MissingInstanceIp { environment_name: String },
2222

23+
#[error("Invalid tracker configuration: {message}")]
24+
InvalidTrackerConfiguration { message: String },
25+
2326
#[error("Invalid state transition: {0}")]
2427
StateTransition(#[from] StateTypeError),
2528

@@ -44,6 +47,9 @@ impl crate::shared::Traceable for TestCommandHandlerError {
4447
"TestCommandHandlerError: Missing instance IP for environment '{environment_name}'"
4548
)
4649
}
50+
Self::InvalidTrackerConfiguration { message } => {
51+
format!("TestCommandHandlerError: Invalid tracker configuration - {message}")
52+
}
4753
Self::StateTransition(e) => {
4854
format!("TestCommandHandlerError: Invalid state transition - {e}")
4955
}
@@ -59,16 +65,17 @@ impl crate::shared::Traceable for TestCommandHandlerError {
5965
Self::EnvironmentNotFound { .. }
6066
| Self::RemoteAction(_)
6167
| Self::MissingInstanceIp { .. }
68+
| Self::InvalidTrackerConfiguration { .. }
6269
| Self::StateTransition(_)
6370
| Self::StatePersistence(_) => None,
6471
}
6572
}
6673

6774
fn error_kind(&self) -> crate::shared::ErrorKind {
6875
match self {
69-
Self::EnvironmentNotFound { .. } | Self::MissingInstanceIp { .. } => {
70-
crate::shared::ErrorKind::Configuration
71-
}
76+
Self::EnvironmentNotFound { .. }
77+
| Self::MissingInstanceIp { .. }
78+
| Self::InvalidTrackerConfiguration { .. } => crate::shared::ErrorKind::Configuration,
7279
Self::Command(_) | Self::RemoteAction(_) => crate::shared::ErrorKind::CommandExecution,
7380
Self::StateTransition(_) | Self::StatePersistence(_) => {
7481
crate::shared::ErrorKind::StatePersistence
@@ -136,6 +143,24 @@ This typically means the environment was created but not provisioned.
136143
3. Then run the test command
137144
138145
For workflow details, see docs/deployment-overview.md"
146+
}
147+
Self::InvalidTrackerConfiguration { .. } => {
148+
"Invalid Tracker Configuration - Troubleshooting:
149+
150+
The tracker configuration in the environment is invalid or incomplete.
151+
152+
1. Check the tracker configuration in your environment file:
153+
cat data/<env-name>/environment.json
154+
155+
2. Verify the HTTP API bind_address format:
156+
Expected: \"0.0.0.0:1212\" (host:port)
157+
158+
3. If needed, recreate the environment with correct configuration:
159+
cargo run -- create template my-config.json
160+
# Edit my-config.json with correct tracker settings
161+
cargo run -- create environment --env-file my-config.json
162+
163+
For tracker configuration details, see docs/user-guide/configuration.md"
139164
}
140165
Self::StateTransition(_) => {
141166
"Invalid State Transition - Troubleshooting:
@@ -211,6 +236,9 @@ mod tests {
211236
TestCommandHandlerError::MissingInstanceIp {
212237
environment_name: "test-env".to_string(),
213238
},
239+
TestCommandHandlerError::InvalidTrackerConfiguration {
240+
message: "Invalid bind address".to_string(),
241+
},
214242
TestCommandHandlerError::StateTransition(StateTypeError::UnexpectedState {
215243
expected: "Provisioned",
216244
actual: "Created".to_string(),

src/application/command_handlers/test/handler.rs

Lines changed: 77 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,33 @@
22
//!
33
//! **Purpose**: Smoke test for running Torrust Tracker services
44
//!
5-
//! This handler validates that a deployed Tracker application is running and accessible.
6-
//! The command is designed for post-deployment verification - checking that services
7-
//! respond correctly to requests, not validating infrastructure components.
5+
//! This handler validates that a deployed Tracker application is running and accessible
6+
//! from external clients. The command performs comprehensive end-to-end verification
7+
//! including service status, health checks, and external accessibility validation.
88
//!
9-
//! **Current Implementation Status**: Work in Progress / Temporary Scaffolding
9+
//! ## Validation Strategy
1010
//!
11-
//! The current validation steps (cloud-init, Docker, Docker Compose) are **temporary
12-
//! scaffolding** that exist only because the complete deployment workflow is not yet
13-
//! implemented. These steps will be **removed** when the full deployment is implemented
14-
//! and replaced with actual smoke tests.
11+
//! The test command validates deployed services through:
1512
//!
16-
//! **Target Implementation** (when `Running` state is implemented):
13+
//! 1. **Docker Compose Service Status** - Verifies containers are running
14+
//! 2. **External Health Checks** - Tests service accessibility from outside the VM:
15+
//! - Tracker API health endpoint (required): `http://<vm-ip>:<api-port>/api/health_check`
16+
//! - HTTP Tracker health endpoint (optional): `http://<vm-ip>:<tracker-port>/api/health_check`
1717
//!
18-
//! - Make HTTP requests to publicly exposed Tracker services
19-
//! - Verify services respond correctly (health checks, basic API calls)
20-
//! - Confirm deployment is production-ready from end-user perspective
18+
//! ## Why External-Only Validation?
19+
//!
20+
//! We perform external accessibility checks (from test runner to VM) rather than
21+
//! internal checks (via SSH to localhost) because:
22+
//! - External checks are a superset of internal checks
23+
//! - If services are accessible externally, they must be running internally
24+
//! - External checks validate firewall configuration automatically
25+
//! - Simpler test implementation reduces maintenance burden
26+
//!
27+
//! ## Port Configuration
28+
//!
29+
//! The test command extracts tracker ports from the environment's tracker configuration:
30+
//! - HTTP API port from `environment.context.user_inputs.tracker.http_api.bind_address`
31+
//! - HTTP Tracker port from `environment.context.user_inputs.tracker.http_trackers[0].bind_address`
2132
//!
2233
//! For rationale and alternatives, see:
2334
//! - `docs/decisions/test-command-as-smoke-test.md` - Architectural decision record
@@ -28,33 +39,29 @@ use tracing::{info, instrument};
2839

2940
use super::errors::TestCommandHandlerError;
3041
use crate::adapters::ssh::SshConfig;
31-
use crate::application::steps::{
32-
ValidateCloudInitCompletionStep, ValidateDockerComposeInstallationStep,
33-
ValidateDockerInstallationStep,
34-
};
3542
use crate::domain::environment::repository::{EnvironmentRepository, TypedEnvironmentRepository};
3643
use crate::domain::EnvironmentName;
44+
use crate::infrastructure::remote_actions::{RemoteAction, RunningServicesValidator};
3745

3846
/// `TestCommandHandler` orchestrates smoke testing for running Torrust Tracker services
3947
///
4048
/// **Purpose**: Post-deployment smoke test to verify the application is running and accessible
4149
///
42-
/// **Current Status**: Work in Progress - Current implementation is temporary scaffolding
50+
/// This handler validates that deployed services are operational and accessible from
51+
/// external clients by performing comprehensive health checks on the Tracker API and
52+
/// HTTP Tracker endpoints.
4353
///
44-
/// The current validation steps are **placeholders** until the complete deployment workflow
45-
/// is implemented with the `Running` state. See module documentation for details.
54+
/// ## Validation Steps
4655
///
47-
/// ## Current Validation Steps (Temporary)
56+
/// 1. **Service Status** - Verifies Docker Compose services are running via SSH
57+
/// 2. **Tracker API Health** (required) - Tests external accessibility of HTTP API
58+
/// 3. **HTTP Tracker Health** (optional) - Tests external accessibility of HTTP tracker
4859
///
49-
/// 1. Validate cloud-init completion
50-
/// 2. Validate Docker installation
51-
/// 3. Validate Docker Compose installation
60+
/// ## Port Discovery
5261
///
53-
/// ## Target Validation Steps (Future)
54-
///
55-
/// 1. HTTP health check to Tracker service
56-
/// 2. Basic API request verification
57-
/// 3. Metrics endpoint validation
62+
/// The handler extracts tracker ports from the environment's tracker configuration:
63+
/// - HTTP API port from `tracker.http_api.bind_address`
64+
/// - HTTP Tracker port from `tracker.http_trackers[0].bind_address`
5865
///
5966
/// ## Design Rationale
6067
///
@@ -80,6 +87,9 @@ impl TestCommandHandler {
8087

8188
/// Execute the complete testing and validation workflow
8289
///
90+
/// Validates that the Torrust Tracker services are running and accessible by
91+
/// performing external health checks on the deployed services.
92+
///
8393
/// # Arguments
8494
///
8595
/// * `env_name` - The name of the environment to test
@@ -89,10 +99,11 @@ impl TestCommandHandler {
8999
/// Returns an error if:
90100
/// * Environment not found
91101
/// * Environment does not have an instance IP set
92-
/// * Any validation step fails:
93-
/// - Cloud-init completion validation fails
94-
/// - Docker installation validation fails
95-
/// - Docker Compose installation validation fails
102+
/// * Tracker configuration is invalid or missing required ports
103+
/// * Running services validation fails:
104+
/// - Services are not running
105+
/// - Health check endpoints are not accessible
106+
/// - Firewall rules block external access
96107
#[instrument(
97108
name = "test_command",
98109
skip_all,
@@ -111,31 +122,54 @@ impl TestCommandHandler {
111122
environment_name: env_name.to_string(),
112123
})?;
113124

125+
// Extract tracker ports from configuration
126+
let tracker_config = any_env.tracker_config();
127+
128+
// Get HTTP API port from bind_address (e.g., "0.0.0.0:1212" -> 1212)
129+
let tracker_api_port =
130+
Self::extract_port_from_bind_address(&tracker_config.http_api.bind_address)
131+
.ok_or_else(|| TestCommandHandlerError::InvalidTrackerConfiguration {
132+
message: format!(
133+
"Invalid HTTP API bind_address: {}. Expected format: 'host:port'",
134+
tracker_config.http_api.bind_address
135+
),
136+
})?;
137+
138+
// Get HTTP Tracker port from first HTTP tracker (optional)
139+
let http_tracker_port = tracker_config
140+
.http_trackers
141+
.first()
142+
.and_then(|tracker| Self::extract_port_from_bind_address(&tracker.bind_address));
143+
114144
let ssh_config =
115145
SshConfig::with_default_port(any_env.ssh_credentials().clone(), instance_ip);
116146

117-
ValidateCloudInitCompletionStep::new(ssh_config.clone())
118-
.execute()
119-
.await?;
147+
// Validate running services with external accessibility checks
148+
let services_validator =
149+
RunningServicesValidator::new(ssh_config, tracker_api_port, http_tracker_port);
120150

121-
ValidateDockerInstallationStep::new(ssh_config.clone())
122-
.execute()
123-
.await?;
124-
125-
ValidateDockerComposeInstallationStep::new(ssh_config)
126-
.execute()
127-
.await?;
151+
services_validator.execute(&instance_ip).await?;
128152

129153
info!(
130154
command = "test",
131155
environment = %env_name,
132156
instance_ip = ?instance_ip,
133-
"Infrastructure testing workflow completed successfully"
157+
tracker_api_port = tracker_api_port,
158+
http_tracker_port = ?http_tracker_port,
159+
"Service testing workflow completed successfully"
134160
);
135161

136162
Ok(())
137163
}
138164

165+
/// Extract port number from bind_address string (e.g., "0.0.0.0:1212" -> Some(1212))
166+
fn extract_port_from_bind_address(bind_address: &str) -> Option<u16> {
167+
bind_address
168+
.split(':')
169+
.nth(1)
170+
.and_then(|port_str| port_str.parse::<u16>().ok())
171+
}
172+
139173
/// Load environment from storage
140174
///
141175
/// # Errors

src/domain/environment/state/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,19 @@ impl AnyEnvironmentState {
427427
self.context().user_inputs.ssh_port
428428
}
429429

430+
/// Get the tracker configuration regardless of current state
431+
///
432+
/// This method provides access to the tracker configuration without needing to
433+
/// pattern match on the specific state variant.
434+
///
435+
/// # Returns
436+
///
437+
/// A reference to the `TrackerConfig` contained within the environment.
438+
#[must_use]
439+
pub fn tracker_config(&self) -> &crate::domain::tracker::TrackerConfig {
440+
&self.context().user_inputs.tracker
441+
}
442+
430443
/// Get the instance IP address if available, regardless of current state
431444
///
432445
/// This method provides access to the instance IP without needing to

0 commit comments

Comments
 (0)