Skip to main content

torrust_tracker_deployer_lib/application/command_handlers/show/
handler.rs

1//! Show command handler implementation
2//!
3//! **Purpose**: Display environment information and status
4//!
5//! This handler retrieves and displays information about an environment
6//! from storage. It is a read-only operation that does not modify any state
7//! or make any network calls.
8//!
9//! ## Display Strategy
10//!
11//! The show command displays state-aware information:
12//!
13//! 1. **Basic Info (all states)**: Environment name, state, provider
14//! 2. **Infrastructure (Provisioned+)**: IP, SSH port, SSH user, SSH key path
15//! 3. **Next Step**: Guidance based on current state
16//!
17//! ## Design Rationale
18//!
19//! This command accepts an `EnvironmentName` in its `execute` method to align with other
20//! command handlers (`ProvisionCommandHandler`, `ConfigureCommandHandler`). This design:
21//!
22//! - Loads environment from repository (consistent pattern across all handlers)
23//! - Allows showing environments regardless of compile-time state (runtime extraction)
24//! - Read-only operation - no state modifications
25
26use std::sync::Arc;
27
28use tracing::instrument;
29
30use super::errors::ShowCommandHandlerError;
31use super::info::{
32    DockerImagesInfo, EnvironmentInfo, GrafanaInfo, InfrastructureInfo, PrometheusInfo, ServiceInfo,
33};
34use crate::domain::environment::repository::EnvironmentRepository;
35use crate::domain::environment::state::AnyEnvironmentState;
36use crate::domain::grafana::GrafanaConfig;
37use crate::domain::mysql::MysqlServiceConfig;
38use crate::domain::prometheus::PrometheusConfig;
39use crate::domain::tracker::config::TrackerConfig;
40use crate::domain::EnvironmentName;
41
42/// Default SSH port when not specified
43const DEFAULT_SSH_PORT: u16 = 22;
44
45/// `ShowCommandHandler` extracts and formats environment information for display
46///
47/// **Purpose**: Read-only information extraction from environment state
48///
49/// This handler loads an environment from storage and extracts information
50/// relevant to the environment's current state. It never modifies state
51/// or makes network calls.
52///
53/// ## Information Extraction
54///
55/// - **All states**: Name, state name, provider
56/// - **Provisioned+**: Infrastructure details (IP, SSH credentials)
57/// - **All states**: Next step guidance
58pub struct ShowCommandHandler {
59    repository: Arc<dyn EnvironmentRepository>,
60}
61
62impl ShowCommandHandler {
63    /// Create a new `ShowCommandHandler`
64    #[must_use]
65    pub fn new(repository: Arc<dyn EnvironmentRepository>) -> Self {
66        Self { repository }
67    }
68
69    /// Execute the show command workflow
70    ///
71    /// Loads the environment and extracts state-aware information for display.
72    ///
73    /// # Arguments
74    ///
75    /// * `env_name` - The name of the environment to show
76    ///
77    /// # Returns
78    ///
79    /// * `Ok(EnvironmentInfo)` - Information about the environment
80    /// * `Err(ShowCommandHandlerError)` - If the environment cannot be loaded
81    ///
82    /// # Errors
83    ///
84    /// Returns an error if:
85    /// * Environment not found
86    /// * Environment state file is corrupted or unreadable
87    #[instrument(
88        name = "show_command",
89        skip_all,
90        fields(
91            command_type = "show",
92            environment = %env_name
93        )
94    )]
95    pub fn execute(
96        &self,
97        env_name: &EnvironmentName,
98    ) -> Result<EnvironmentInfo, ShowCommandHandlerError> {
99        let any_env = self.load_environment(env_name)?;
100
101        Ok(Self::extract_info(&any_env))
102    }
103
104    /// Load environment from repository
105    fn load_environment(
106        &self,
107        env_name: &EnvironmentName,
108    ) -> Result<AnyEnvironmentState, ShowCommandHandlerError> {
109        if !self.repository.exists(env_name)? {
110            return Err(ShowCommandHandlerError::EnvironmentNotFound {
111                name: env_name.to_string(),
112            });
113        }
114
115        self.repository.load(env_name)?.ok_or_else(|| {
116            ShowCommandHandlerError::EnvironmentNotFound {
117                name: env_name.to_string(),
118            }
119        })
120    }
121
122    /// Extract information from environment based on its state
123    fn extract_info(any_env: &AnyEnvironmentState) -> EnvironmentInfo {
124        let name = any_env.name().to_string();
125        let state = any_env.state_display_name().to_string();
126        let provider = any_env.provider_display_name().to_string();
127        let created_at = any_env.created_at();
128        let state_name = any_env.state_name().to_string();
129
130        let tracker_config = any_env.tracker_config();
131        let docker_images = DockerImagesInfo::new(
132            TrackerConfig::docker_image().full_reference(),
133            if tracker_config.uses_mysql() {
134                Some(MysqlServiceConfig::docker_image().full_reference())
135            } else {
136                None
137            },
138            any_env
139                .prometheus_config()
140                .map(|_| PrometheusConfig::docker_image().full_reference()),
141            any_env
142                .grafana_config()
143                .map(|_| GrafanaConfig::docker_image().full_reference()),
144        );
145
146        let mut info =
147            EnvironmentInfo::new(name, state, provider, created_at, docker_images, state_name);
148
149        // Add infrastructure info if instance IP is available
150        if let Some(instance_ip) = any_env.instance_ip() {
151            let ssh_creds = any_env.ssh_credentials();
152            let ssh_port = any_env.ssh_port();
153
154            let infra = InfrastructureInfo::new(
155                instance_ip,
156                if ssh_port == 0 {
157                    DEFAULT_SSH_PORT
158                } else {
159                    ssh_port
160                },
161                ssh_creds.ssh_username.to_string(),
162                ssh_creds.ssh_priv_key_path.to_string_lossy().to_string(),
163            );
164            info = info.with_infrastructure(infra);
165
166            // Add service info for Released/Running states
167            if Self::should_show_services(any_env.state_name()) {
168                // Always compute from tracker config to show proper service information
169                // including TLS domains, localhost hints, and HTTPS status
170                let grafana_config = any_env.grafana_config();
171                let services =
172                    ServiceInfo::from_tracker_config(tracker_config, instance_ip, grafana_config);
173                info = info.with_services(services);
174
175                // Add Prometheus info if configured
176                if any_env.prometheus_config().is_some() {
177                    info = info.with_prometheus(PrometheusInfo::default_internal());
178                }
179
180                // Add Grafana info if configured
181                if let Some(grafana) = any_env.grafana_config() {
182                    info = info.with_grafana(GrafanaInfo::from_config(grafana, instance_ip));
183                }
184            }
185        }
186
187        info
188    }
189
190    /// Determine if services should be shown based on state
191    ///
192    /// Services are shown for states where the tracker configuration has been
193    /// deployed and services may be running (Released, Running, or related failed states).
194    fn should_show_services(state_name: &str) -> bool {
195        matches!(
196            state_name,
197            "released" | "running" | "release_failed" | "run_failed"
198        )
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    mod should_show_services {
207        use super::*;
208
209        #[test]
210        fn it_should_show_services_for_released_state() {
211            assert!(ShowCommandHandler::should_show_services("released"));
212        }
213
214        #[test]
215        fn it_should_show_services_for_running_state() {
216            assert!(ShowCommandHandler::should_show_services("running"));
217        }
218
219        #[test]
220        fn it_should_show_services_for_release_failed_state() {
221            assert!(ShowCommandHandler::should_show_services("release_failed"));
222        }
223
224        #[test]
225        fn it_should_show_services_for_run_failed_state() {
226            assert!(ShowCommandHandler::should_show_services("run_failed"));
227        }
228
229        #[test]
230        fn it_should_not_show_services_for_created_state() {
231            assert!(!ShowCommandHandler::should_show_services("created"));
232        }
233
234        #[test]
235        fn it_should_not_show_services_for_provisioned_state() {
236            assert!(!ShowCommandHandler::should_show_services("provisioned"));
237        }
238
239        #[test]
240        fn it_should_not_show_services_for_configured_state() {
241            assert!(!ShowCommandHandler::should_show_services("configured"));
242        }
243    }
244}