use std::net::SocketAddr;
use thiserror::Error;
use tracing::info;
use crate::adapters::ssh::SshConfig;
use crate::adapters::ssh::SshCredentials;
use crate::infrastructure::remote_actions::{RemoteAction, RemoteActionError};
#[derive(Debug, Clone, Copy, Default)]
pub struct ServiceValidation {
pub prometheus: bool,
pub grafana: bool,
}
const DEFAULT_DEPLOY_DIR: &str = "/opt/torrust";
const DEFAULT_PROMETHEUS_CONFIG_DIR: &str = "/opt/torrust/storage/prometheus/etc";
#[derive(Debug, Error)]
pub enum ReleaseValidationError {
#[error(
"Docker Compose files validation failed: {source}
Tip: Ensure the release command completed successfully and files were deployed"
)]
ComposeFilesValidationFailed {
#[source]
source: RemoteActionError,
},
#[error(
"Prometheus configuration files validation failed: {source}
Tip: Ensure Prometheus is configured in environment config and release command completed successfully"
)]
PrometheusConfigValidationFailed {
#[source]
source: RemoteActionError,
},
}
impl ReleaseValidationError {
#[must_use]
pub fn help(&self) -> &'static str {
match self {
Self::ComposeFilesValidationFailed { .. } => {
"Docker Compose Files Validation Failed - Detailed Troubleshooting:
1. Check if release command completed:
- SSH to instance: ssh user@instance-ip
- Check deployment directory: ls -la /opt/torrust/
- Verify docker-compose.yml exists
2. Verify file deployment:
- Check Ansible deployment logs for errors
- Verify the release command ran without errors
- Ensure source template files exist in templates/docker-compose/
3. Common issues:
- Deployment directory not created: mkdir -p /opt/torrust
- Insufficient permissions to write files
- Ansible playbook failed silently
- Template rendering errors
4. Re-deploy if needed:
- Re-run release command: cargo run -- release <environment>
- Or manually copy files to /opt/torrust/
For more information, see docs/e2e-testing/."
}
Self::PrometheusConfigValidationFailed { .. } => {
"Prometheus Configuration Files Validation Failed - Detailed Troubleshooting:
1. Check if Prometheus is enabled in environment config:
- Verify prometheus section exists in envs/<env-name>.json
- Ensure prometheus.scrape_interval is set (e.g., 15)
2. Check if release command completed:
- SSH to instance: ssh user@instance-ip
- Check Prometheus directory: ls -la /opt/torrust/storage/prometheus/etc/
- Verify prometheus.yml exists
3. Verify file deployment:
- Check Ansible deployment logs for errors
- Verify the release command ran without errors
- Ensure source template files exist in templates/prometheus/
4. Common issues:
- Prometheus section missing from environment config (intentional if disabled)
- Storage directory not created: mkdir -p /opt/torrust/storage/prometheus/etc
- Insufficient permissions to write files
- Ansible playbook failed silently
- Template rendering errors
5. Re-deploy if needed:
- Re-run release command: cargo run -- release <environment>
- Or manually copy files to /opt/torrust/storage/prometheus/etc/
For more information, see docs/e2e-testing/."
}
}
}
}
struct ComposeFilesValidator {
ssh_client: crate::adapters::ssh::SshClient,
deploy_dir: std::path::PathBuf,
}
impl ComposeFilesValidator {
#[must_use]
fn new(ssh_config: SshConfig) -> Self {
let ssh_client = crate::adapters::ssh::SshClient::new(ssh_config);
Self {
ssh_client,
deploy_dir: std::path::PathBuf::from(DEFAULT_DEPLOY_DIR),
}
}
}
impl RemoteAction for ComposeFilesValidator {
fn name(&self) -> &'static str {
"compose-files-validation"
}
async fn execute(&self, server_ip: &std::net::IpAddr) -> Result<(), RemoteActionError> {
info!(
action = "compose_files_validation",
deploy_dir = %self.deploy_dir.display(),
server_ip = %server_ip,
"Validating Docker Compose files are deployed"
);
let deploy_dir = self.deploy_dir.display();
let command = format!("test -f {deploy_dir}/docker-compose.yml && echo 'exists'");
let output = self.ssh_client.execute(&command).map_err(|source| {
RemoteActionError::SshCommandFailed {
action_name: self.name().to_string(),
source,
}
})?;
if !output.trim().contains("exists") {
return Err(RemoteActionError::ValidationFailed {
action_name: self.name().to_string(),
message: format!(
"docker-compose.yml not found in {deploy_dir}. \
Ensure the release command completed successfully."
),
});
}
info!(
action = "compose_files_validation",
status = "success",
"Docker Compose files are deployed correctly"
);
Ok(())
}
}
struct PrometheusConfigValidator {
ssh_client: crate::adapters::ssh::SshClient,
config_dir: std::path::PathBuf,
}
impl PrometheusConfigValidator {
#[must_use]
fn new(ssh_config: SshConfig) -> Self {
let ssh_client = crate::adapters::ssh::SshClient::new(ssh_config);
Self {
ssh_client,
config_dir: std::path::PathBuf::from(DEFAULT_PROMETHEUS_CONFIG_DIR),
}
}
}
impl RemoteAction for PrometheusConfigValidator {
fn name(&self) -> &'static str {
"prometheus-config-validation"
}
async fn execute(&self, server_ip: &std::net::IpAddr) -> Result<(), RemoteActionError> {
info!(
action = "prometheus_config_validation",
config_dir = %self.config_dir.display(),
server_ip = %server_ip,
"Validating Prometheus configuration files are deployed"
);
let config_dir = self.config_dir.display();
let command = format!("test -f {config_dir}/prometheus.yml && echo 'exists'");
let output = self.ssh_client.execute(&command).map_err(|source| {
RemoteActionError::SshCommandFailed {
action_name: self.name().to_string(),
source,
}
})?;
if !output.trim().contains("exists") {
return Err(RemoteActionError::ValidationFailed {
action_name: self.name().to_string(),
message: format!(
"prometheus.yml not found in {config_dir}. \
Ensure Prometheus is configured and release command completed successfully."
),
});
}
info!(
action = "prometheus_config_validation",
status = "success",
"Prometheus configuration files are deployed correctly"
);
Ok(())
}
}
pub async fn run_release_validation(
socket_addr: SocketAddr,
ssh_credentials: &SshCredentials,
services: Option<ServiceValidation>,
) -> Result<(), ReleaseValidationError> {
let services = services.unwrap_or_default();
info!(
socket_addr = %socket_addr,
ssh_user = %ssh_credentials.ssh_username,
validate_prometheus = services.prometheus,
validate_grafana = services.grafana,
"Running release validation tests"
);
let ip_addr = socket_addr.ip();
validate_compose_files(ip_addr, ssh_credentials, socket_addr.port()).await?;
if services.prometheus {
validate_prometheus_config(ip_addr, ssh_credentials, socket_addr.port()).await?;
}
info!(
socket_addr = %socket_addr,
status = "success",
"All release validation tests passed successfully"
);
Ok(())
}
async fn validate_compose_files(
ip_addr: std::net::IpAddr,
ssh_credentials: &SshCredentials,
port: u16,
) -> Result<(), ReleaseValidationError> {
info!("Validating Docker Compose files deployment");
let ssh_config = SshConfig::new(ssh_credentials.clone(), SocketAddr::new(ip_addr, port));
let validator = ComposeFilesValidator::new(ssh_config);
validator
.execute(&ip_addr)
.await
.map_err(|source| ReleaseValidationError::ComposeFilesValidationFailed { source })?;
Ok(())
}
async fn validate_prometheus_config(
ip_addr: std::net::IpAddr,
ssh_credentials: &SshCredentials,
port: u16,
) -> Result<(), ReleaseValidationError> {
info!("Validating Prometheus configuration files deployment");
let ssh_config = SshConfig::new(ssh_credentials.clone(), SocketAddr::new(ip_addr, port));
let validator = PrometheusConfigValidator::new(ssh_config);
validator
.execute(&ip_addr)
.await
.map_err(|source| ReleaseValidationError::PrometheusConfigValidationFailed { source })?;
Ok(())
}