Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions src/application/command_handlers/list/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
//! Error types for list command handler

use std::path::PathBuf;

use crate::shared::error::kind::ErrorKind;
use crate::shared::error::traceable::Traceable;

/// Comprehensive error type for the `ListCommandHandler`
#[derive(Debug, thiserror::Error)]
pub enum ListCommandHandlerError {
/// Data directory not found
#[error("Data directory not found: '{path}'")]
DataDirectoryNotFound { path: PathBuf },

/// Permission denied accessing directory
#[error("Permission denied accessing directory: '{path}'")]
PermissionDenied { path: PathBuf },

/// Failed to scan environments directory
#[error("Failed to scan environments directory: {message}")]
ScanError { message: String },
}

impl Traceable for ListCommandHandlerError {
fn trace_format(&self) -> String {
match self {
Self::DataDirectoryNotFound { path } => {
format!(
"ListCommandHandlerError: Data directory not found - '{}'",
path.display()
)
}
Self::ScanError { message } => {
format!("ListCommandHandlerError: Scan error - {message}")
}
Self::PermissionDenied { path } => {
format!(
"ListCommandHandlerError: Permission denied - '{}'",
path.display()
)
}
}
}

fn trace_source(&self) -> Option<&dyn Traceable> {
None
}

fn error_kind(&self) -> ErrorKind {
match self {
Self::DataDirectoryNotFound { .. }
| Self::ScanError { .. }
| Self::PermissionDenied { .. } => ErrorKind::FileSystem,
}
}
}

impl ListCommandHandlerError {
/// Provides detailed troubleshooting guidance for this error
///
/// # Example
///
/// ```
/// use std::path::PathBuf;
/// use torrust_tracker_deployer_lib::application::command_handlers::list::errors::ListCommandHandlerError;
///
/// let error = ListCommandHandlerError::DataDirectoryNotFound {
/// path: PathBuf::from("./data"),
/// };
///
/// let help = error.help();
/// assert!(help.contains("Verify current directory"));
/// ```
#[must_use]
pub fn help(&self) -> &'static str {
match self {
Self::DataDirectoryNotFound { .. } => {
"Data Directory Not Found - Troubleshooting:

1. Verify current directory:
- Run: pwd
- Expected: Your deployer workspace directory

2. Check if data directory exists:
- Run: ls -la data/
- Should contain environment subdirectories

3. Create environment first:
- Run: torrust-tracker-deployer create environment --env-file <config.json>

Common causes:
- Running from the wrong directory
- No environments have been created yet
- Data directory was moved or deleted

For more information, see docs/user-guide/commands.md"
}
Self::ScanError { .. } => {
"Scan Error - Troubleshooting:

1. Check directory permissions:
- Run: ls -ld data/
- Should have read permission (r--)

2. Verify filesystem health:
- Check for disk errors or filesystem issues

3. Try running with elevated permissions if needed

Common causes:
- File system errors
- Corrupted directory entries
- Network filesystem issues

For more information, see docs/user-guide/commands.md"
}
Self::PermissionDenied { .. } => {
"Permission Denied - Troubleshooting:

1. Check directory permissions:
- Run: ls -ld data/
- Should have read permission (r--)

2. Check file permissions:
- Run: ls -l data/*/environment.json
- Should have read permission (r--)

3. Fix permissions if needed:
- Run: chmod +rx data/
- Run: chmod +r data/*/environment.json

Common causes:
- File created by different user
- Restrictive umask settings
- SELinux or AppArmor restrictions

For more information, see docs/user-guide/commands.md"
}
}
}
}
224 changes: 224 additions & 0 deletions src/application/command_handlers/list/handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
//! List command handler implementation
//!
//! **Purpose**: List all environments in the deployment workspace
//!
//! This handler scans the data directory for environments and extracts
//! summary information for display. It is a read-only operation that
//! does not modify any state or make any network calls.
//!
//! ## Design Strategy
//!
//! The list command scans local storage for environments:
//!
//! 1. **Directory Scan**: Find all environment directories in data/
//! 2. **Load Summaries**: Extract lightweight info from each environment
//! 3. **Graceful Degradation**: Continue on per-environment errors
//! 4. **Report Failures**: Include failed environments in the result
//!
//! ## Design Rationale
//!
//! This command works directly with the data directory rather than through
//! the repository abstraction because:
//!
//! - Need to enumerate all environments (repository has no list method)
//! - Must handle partially corrupted data gracefully
//! - Performance: lightweight scanning without full deserialization where possible

use std::fs;
use std::path::Path;
use std::sync::Arc;

use tracing::{instrument, warn};

use super::errors::ListCommandHandlerError;
use super::info::{EnvironmentList, EnvironmentSummary};
use crate::domain::environment::name::EnvironmentName;
use crate::domain::environment::repository::EnvironmentRepository;
use crate::domain::environment::state::AnyEnvironmentState;
use crate::infrastructure::persistence::repository_factory::RepositoryFactory;

/// `ListCommandHandler` scans and lists all environments
///
/// **Purpose**: Read-only enumeration of environments in the workspace
///
/// This handler scans the data directory and extracts summary information
/// for each environment found. It handles partial failures gracefully,
/// continuing to list valid environments even when some fail to load.
///
/// ## Error Handling
///
/// - **Empty directory**: Returns empty list (not an error)
/// - **Per-environment errors**: Collected and reported, don't stop listing
/// - **Fatal errors**: Directory not found, permission denied
pub struct ListCommandHandler {
repository_factory: Arc<RepositoryFactory>,
data_directory: Arc<Path>,
}

impl ListCommandHandler {
/// Create a new `ListCommandHandler`
#[must_use]
pub fn new(repository_factory: Arc<RepositoryFactory>, data_directory: Arc<Path>) -> Self {
Self {
repository_factory,
data_directory,
}
}

/// Execute the list command workflow
///
/// Scans the data directory and extracts summary information for all
/// environments found.
///
/// # Returns
///
/// * `Ok(EnvironmentList)` - List of environment summaries (may include failures)
/// * `Err(ListCommandHandlerError)` - If the data directory cannot be accessed
///
/// # Errors
///
/// Returns an error if:
/// * Data directory does not exist
/// * Permission denied accessing data directory
#[instrument(
name = "list_command",
skip_all,
fields(
command_type = "list",
data_directory = %self.data_directory.display()
)
)]
pub fn execute(&self) -> Result<EnvironmentList, ListCommandHandlerError> {
// Verify data directory exists
if !self.data_directory.exists() {
return Err(ListCommandHandlerError::DataDirectoryNotFound {
path: self.data_directory.to_path_buf(),
});
}

// Scan for environment directories
let env_dirs = self.scan_environment_directories()?;

// Load summaries for each environment
let (summaries, failures) = self.load_environment_summaries(&env_dirs);

Ok(EnvironmentList::new(
summaries,
failures,
self.data_directory.to_string_lossy().to_string(),
))
}

/// Scan the data directory for environment subdirectories
fn scan_environment_directories(&self) -> Result<Vec<String>, ListCommandHandlerError> {
let entries = fs::read_dir(&self.data_directory).map_err(|e| {
if e.kind() == std::io::ErrorKind::PermissionDenied {
ListCommandHandlerError::PermissionDenied {
path: self.data_directory.to_path_buf(),
}
} else {
ListCommandHandlerError::ScanError {
message: e.to_string(),
}
}
})?;

let mut env_names = Vec::new();

for entry in entries {
let entry = match entry {
Ok(e) => e,
Err(e) => {
warn!("Failed to read directory entry: {e}");
continue;
}
};

// Only consider directories (environments are stored in subdirectories)
let path = entry.path();
if !path.is_dir() {
continue;
}

// Check if this directory contains an environment.json file
let env_file = path.join("environment.json");
if !env_file.exists() {
continue;
}

// Extract directory name as environment name
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
env_names.push(name.to_string());
}
}

Ok(env_names)
}

/// Load summaries for all discovered environments
///
/// Returns a tuple of (successful summaries, failed environments)
fn load_environment_summaries(
&self,
env_names: &[String],
) -> (Vec<EnvironmentSummary>, Vec<(String, String)>) {
let mut summaries = Vec::new();
let mut failures = Vec::new();

for name in env_names {
match self.load_environment_summary(name) {
Ok(summary) => summaries.push(summary),
Err(error) => {
warn!(
environment = %name,
error = %error,
"Failed to load environment"
);
failures.push((name.clone(), error));
}
}
}

(summaries, failures)
}

/// Load summary for a single environment
fn load_environment_summary(&self, name: &str) -> Result<EnvironmentSummary, String> {
// Validate environment name
let env_name = EnvironmentName::new(name.to_string())
.map_err(|e| format!("Invalid environment name: {e}"))?;

// Create repository for the base data directory
// (repository internally handles {base_dir}/{env_name}/environment.json)
let repository = self
.repository_factory
.create(self.data_directory.to_path_buf());

// Load environment from repository
let any_env = Self::load_environment(&repository, &env_name)?;

// Extract summary
Ok(Self::extract_summary(&any_env))
}

/// Load environment from repository
fn load_environment(
repository: &Arc<dyn EnvironmentRepository + Send + Sync>,
env_name: &EnvironmentName,
) -> Result<AnyEnvironmentState, String> {
repository
.load(env_name)
.map_err(|e| format!("Failed to load environment: {e}"))?
.ok_or_else(|| format!("Environment '{env_name}' not found in repository"))
}

/// Extract summary information from an environment
fn extract_summary(any_env: &AnyEnvironmentState) -> EnvironmentSummary {
let name = any_env.name().to_string();
let state = any_env.state_display_name().to_string();
let provider = any_env.provider_display_name().to_string();
let created_at = any_env.created_at().to_rfc3339();

EnvironmentSummary::new(name, state, provider, created_at)
}
}
Loading
Loading