From 68eb4d021ec9816f34997e9166805e983552aca8 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 17 Feb 2026 11:47:54 +0000 Subject: [PATCH 1/3] feat: [#359] add JSON output to list command --- src/application/command_handlers/list/info.rs | 6 +- src/presentation/controllers/list/handler.rs | 30 ++- src/presentation/dispatch/router.rs | 6 +- src/presentation/views/commands/list/mod.rs | 10 +- .../views/commands/list/views/json_view.rs | 249 ++++++++++++++++++ 5 files changed, 288 insertions(+), 13 deletions(-) create mode 100644 src/presentation/views/commands/list/views/json_view.rs diff --git a/src/application/command_handlers/list/info.rs b/src/application/command_handlers/list/info.rs index 7233b0265..947f46517 100644 --- a/src/application/command_handlers/list/info.rs +++ b/src/application/command_handlers/list/info.rs @@ -4,12 +4,14 @@ //! for list display purposes. They provide a clean separation between the domain //! model and the presentation layer. +use serde::Serialize; + /// Lightweight environment summary for list display /// /// This DTO contains minimal information about an environment suitable for /// display in a list view. It is designed to be fast to extract and small /// in memory footprint. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct EnvironmentSummary { /// Name of the environment pub name: String, @@ -41,7 +43,7 @@ impl EnvironmentSummary { /// /// This DTO wraps a list of environment summaries along with metadata /// about the listing operation, including any partial failures encountered. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct EnvironmentList { /// Successfully loaded environment summaries pub environments: Vec, diff --git a/src/presentation/controllers/list/handler.rs b/src/presentation/controllers/list/handler.rs index df5ead6fc..eb37eda68 100644 --- a/src/presentation/controllers/list/handler.rs +++ b/src/presentation/controllers/list/handler.rs @@ -12,7 +12,8 @@ use parking_lot::ReentrantMutex; use crate::application::command_handlers::list::info::EnvironmentList; use crate::application::command_handlers::list::{ListCommandHandler, ListCommandHandlerError}; use crate::infrastructure::persistence::repository_factory::RepositoryFactory; -use crate::presentation::views::commands::list::TextView; +use crate::presentation::input::cli::output_format::OutputFormat; +use crate::presentation::views::commands::list::{JsonView, TextView}; use crate::presentation::views::progress::ProgressReporter; use crate::presentation::views::UserOutput; @@ -90,15 +91,19 @@ impl ListCommandController { /// 1. Scan for environments via application layer /// 2. Display results to user /// + /// # Arguments + /// + /// * `output_format` - Output format (Text or Json) + /// /// # Errors /// /// Returns `ListSubcommandError` if any step fails - pub fn execute(&mut self) -> Result<(), ListSubcommandError> { + pub fn execute(&mut self, output_format: OutputFormat) -> Result<(), ListSubcommandError> { // Step 1: Scan for environments via application layer let env_list = self.scan_environments()?; // Step 2: Display results - self.display_results(&env_list)?; + self.display_results(&env_list, output_format)?; Ok(()) } @@ -139,12 +144,27 @@ impl ListCommandController { /// /// The output is written to stdout (not stderr) as it represents the final /// command result rather than progress information. - fn display_results(&mut self, env_list: &EnvironmentList) -> Result<(), ListSubcommandError> { + /// + /// # Arguments + /// + /// * `env_list` - Environment list to display + /// * `output_format` - Output format (Text or Json) + fn display_results( + &mut self, + env_list: &EnvironmentList, + output_format: OutputFormat, + ) -> Result<(), ListSubcommandError> { self.progress .start_step(ListStep::DisplayResults.description())?; // Pipeline: EnvironmentList → render → output to stdout - self.progress.result(&TextView::render(env_list))?; + // Use Strategy Pattern to select view based on output format + let output = match output_format { + OutputFormat::Text => TextView::render(env_list), + OutputFormat::Json => JsonView::render(env_list), + }; + + self.progress.result(&output)?; self.progress.complete_step(Some("Results displayed"))?; diff --git a/src/presentation/dispatch/router.rs b/src/presentation/dispatch/router.rs index f80114994..adade8d0e 100644 --- a/src/presentation/dispatch/router.rs +++ b/src/presentation/dispatch/router.rs @@ -218,7 +218,11 @@ pub async fn route_command( Ok(()) } Commands::List => { - context.container().create_list_controller().execute()?; + let output_format = context.output_format(); + context + .container() + .create_list_controller() + .execute(output_format)?; Ok(()) } } diff --git a/src/presentation/views/commands/list/mod.rs b/src/presentation/views/commands/list/mod.rs index d69bb285b..31fdb8cc5 100644 --- a/src/presentation/views/commands/list/mod.rs +++ b/src/presentation/views/commands/list/mod.rs @@ -6,22 +6,22 @@ //! //! This module follows the Strategy Pattern for rendering: //! - `TextView`: Renders human-readable text table output +//! - `JsonView`: Renders machine-readable JSON output //! //! # Structure //! //! - `views/`: View rendering implementations //! - `text_view.rs`: Human-readable table rendering -//! -//! # Future Expansion -//! -//! When JSON output support is added (EPIC #348 task 12.5), create `views/json_view.rs`. +//! - `json_view.rs`: JSON output for automation workflows pub mod views { + pub mod json_view; pub mod text_view; // Re-export main types for convenience + pub use json_view::JsonView; pub use text_view::TextView; } // Re-export everything at the module level for backward compatibility -pub use views::TextView; +pub use views::{JsonView, TextView}; diff --git a/src/presentation/views/commands/list/views/json_view.rs b/src/presentation/views/commands/list/views/json_view.rs new file mode 100644 index 000000000..c6b34b7ec --- /dev/null +++ b/src/presentation/views/commands/list/views/json_view.rs @@ -0,0 +1,249 @@ +//! JSON View for Environment List +//! +//! This module provides JSON-based rendering for the environment list command. +//! It follows the Strategy Pattern, providing a machine-readable output format +//! for the same underlying data (`EnvironmentList` DTO). +//! +//! # Design +//! +//! The `JsonView` serializes environment list information to JSON using `serde_json`. +//! The output includes environment summaries, failed environments, and metadata. + +use crate::application::command_handlers::list::info::EnvironmentList; + +/// View for rendering environment list as JSON +/// +/// This view provides machine-readable JSON output for automation workflows +/// and AI agents. It serializes the environment list without any transformations, +/// preserving all field names and structure from the domain DTOs. +/// +/// # Examples +/// +/// ```rust +/// use torrust_tracker_deployer_lib::application::command_handlers::list::info::{ +/// EnvironmentList, EnvironmentSummary, +/// }; +/// use torrust_tracker_deployer_lib::presentation::views::commands::list::JsonView; +/// +/// let summaries = vec![ +/// EnvironmentSummary::new( +/// "production-tracker".to_string(), +/// "Running".to_string(), +/// "LXD".to_string(), +/// "2026-02-14T16:45:00Z".to_string(), +/// ), +/// ]; +/// +/// let list = EnvironmentList::new(summaries, vec![], "/path/to/data".to_string()); +/// let output = JsonView::render(&list); +/// +/// // Verify it's valid JSON +/// let parsed: serde_json::Value = serde_json::from_str(&output).unwrap(); +/// assert_eq!(parsed["total_count"], 1); +/// ``` +pub struct JsonView; + +impl JsonView { + /// Render environment list as JSON + /// + /// Serializes the environment list to pretty-printed JSON format. + /// The JSON structure matches the DTO structure exactly: + /// - `environments`: Array of environment summaries + /// - `total_count`: Number of successfully loaded environments + /// - `failed_environments`: Array of failures (name, error pairs) + /// - `data_directory`: Path to scanned directory + /// + /// # Arguments + /// + /// * `list` - Environment list to render + /// + /// # Returns + /// + /// A JSON string containing the serialized environment list. + /// If serialization fails (which should never happen with valid data), + /// returns an error JSON object with the serialization error message. + /// + /// # Examples + /// + /// ```rust + /// use torrust_tracker_deployer_lib::application::command_handlers::list::info::{ + /// EnvironmentList, EnvironmentSummary, + /// }; + /// use torrust_tracker_deployer_lib::presentation::views::commands::list::JsonView; + /// + /// let summaries = vec![ + /// EnvironmentSummary::new( + /// "env1".to_string(), + /// "Running".to_string(), + /// "LXD".to_string(), + /// "2026-01-05T10:30:00Z".to_string(), + /// ), + /// EnvironmentSummary::new( + /// "env2".to_string(), + /// "Created".to_string(), + /// "Hetzner".to_string(), + /// "2026-01-06T14:15:30Z".to_string(), + /// ), + /// ]; + /// + /// let list = EnvironmentList::new(summaries, vec![], "/data".to_string()); + /// let json = JsonView::render(&list); + /// + /// assert!(json.contains("\"total_count\": 2")); + /// assert!(json.contains("\"env1\"")); + /// assert!(json.contains("\"env2\"")); + /// ``` + #[must_use] + pub fn render(list: &EnvironmentList) -> String { + serde_json::to_string_pretty(list).unwrap_or_else(|e| { + format!( + r#"{{ + "error": "Failed to serialize environment list", + "message": "{e}" +}}"# + ) + }) + } +} + +#[cfg(test)] +mod tests { + use serde_json::Value; + + use super::*; + use crate::application::command_handlers::list::info::EnvironmentSummary; + + #[test] + fn it_should_render_empty_environment_list_as_json() { + let list = EnvironmentList::new(vec![], vec![], "/path/to/data".to_string()); + + let output = JsonView::render(&list); + + // Verify it's valid JSON + let parsed: Value = serde_json::from_str(&output).expect("Should be valid JSON"); + + assert_eq!(parsed["total_count"], 0); + assert_eq!(parsed["environments"].as_array().unwrap().len(), 0); + assert_eq!(parsed["failed_environments"].as_array().unwrap().len(), 0); + assert_eq!(parsed["data_directory"], "/path/to/data"); + } + + #[test] + fn it_should_render_single_environment_as_json() { + let summaries = vec![EnvironmentSummary::new( + "my-production".to_string(), + "Running".to_string(), + "LXD".to_string(), + "2026-02-14T16:45:00Z".to_string(), + )]; + + let list = EnvironmentList::new(summaries, vec![], "/data".to_string()); + + let output = JsonView::render(&list); + + let parsed: Value = serde_json::from_str(&output).expect("Should be valid JSON"); + + assert_eq!(parsed["total_count"], 1); + + let envs = parsed["environments"].as_array().unwrap(); + assert_eq!(envs.len(), 1); + assert_eq!(envs[0]["name"], "my-production"); + assert_eq!(envs[0]["state"], "Running"); + assert_eq!(envs[0]["provider"], "LXD"); + assert_eq!(envs[0]["created_at"], "2026-02-14T16:45:00Z"); + } + + #[test] + fn it_should_render_multiple_environments_as_json() { + let summaries = vec![ + EnvironmentSummary::new( + "env1".to_string(), + "Running".to_string(), + "LXD".to_string(), + "2026-01-05T10:30:00Z".to_string(), + ), + EnvironmentSummary::new( + "env2".to_string(), + "Created".to_string(), + "Hetzner Cloud".to_string(), + "2026-01-06T14:15:30Z".to_string(), + ), + EnvironmentSummary::new( + "staging-high-availability-tracker".to_string(), + "Provisioned".to_string(), + "LXD".to_string(), + "2026-01-10T09:00:00Z".to_string(), + ), + ]; + + let list = EnvironmentList::new(summaries, vec![], "/workspace/data".to_string()); + + let output = JsonView::render(&list); + + let parsed: Value = serde_json::from_str(&output).expect("Should be valid JSON"); + + assert_eq!(parsed["total_count"], 3); + + let envs = parsed["environments"].as_array().unwrap(); + assert_eq!(envs.len(), 3); + + // Verify full names are preserved (no truncation) + assert_eq!(envs[2]["name"], "staging-high-availability-tracker"); + } + + #[test] + fn it_should_include_failed_environments_in_json() { + let summaries = vec![EnvironmentSummary::new( + "working-env".to_string(), + "Running".to_string(), + "LXD".to_string(), + "2026-01-05T10:30:00Z".to_string(), + )]; + + let failures = vec![ + ( + "broken-env-1".to_string(), + "Invalid JSON format".to_string(), + ), + ( + "broken-env-2".to_string(), + "Missing required field".to_string(), + ), + ]; + + let list = EnvironmentList::new(summaries, failures, "/data".to_string()); + + let output = JsonView::render(&list); + + let parsed: Value = serde_json::from_str(&output).expect("Should be valid JSON"); + + assert_eq!(parsed["total_count"], 1); + + let failed = parsed["failed_environments"].as_array().unwrap(); + assert_eq!(failed.len(), 2); + + // Failed environments are represented as [name, error] tuples + assert_eq!(failed[0][0], "broken-env-1"); + assert_eq!(failed[0][1], "Invalid JSON format"); + assert_eq!(failed[1][0], "broken-env-2"); + assert_eq!(failed[1][1], "Missing required field"); + } + + #[test] + fn it_should_produce_pretty_printed_json() { + let summaries = vec![EnvironmentSummary::new( + "test".to_string(), + "Running".to_string(), + "LXD".to_string(), + "2026-01-05T10:30:00Z".to_string(), + )]; + + let list = EnvironmentList::new(summaries, vec![], "/data".to_string()); + + let output = JsonView::render(&list); + + // Pretty-printed JSON should have newlines and indentation + assert!(output.contains('\n')); + assert!(output.contains(" ")); + } +} From bda241291b698a4634a59e6cc6ee0e463a7a80fc Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 17 Feb 2026 11:54:54 +0000 Subject: [PATCH 2/3] refactor: [#359] increase environment name column width from 20 to 50 chars --- .../views/commands/list/views/text_view.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/presentation/views/commands/list/views/text_view.rs b/src/presentation/views/commands/list/views/text_view.rs index 84819e707..c98f5eb99 100644 --- a/src/presentation/views/commands/list/views/text_view.rs +++ b/src/presentation/views/commands/list/views/text_view.rs @@ -121,14 +121,14 @@ impl TextView { /// Render table header row fn render_table_header() -> String { format!( - "{:<20} {:<18} {:<14} {}", + "{:<50} {:<18} {:<14} {}", "Name", "State", "Provider", "Created" ) } /// Render table separator fn render_table_separator() -> String { - "─".repeat(76) + "─".repeat(106) } /// Render a single table row @@ -136,8 +136,8 @@ impl TextView { env: &crate::application::command_handlers::list::info::EnvironmentSummary, ) -> String { format!( - "{:<20} {:<18} {:<14} {}", - Self::truncate(&env.name, 20), + "{:<50} {:<18} {:<14} {}", + Self::truncate(&env.name, 50), Self::truncate(&env.state, 18), Self::truncate(&env.provider, 14), &env.created_at @@ -262,8 +262,8 @@ mod tests { let output = TextView::render(&list); - // Should truncate the long name - assert!(output.contains("very-long-environ...")); + // Should truncate the long name at 50 characters + assert!(output.contains("very-long-environment-name-that-exceeds-column-...")); assert!(output .contains("Hint: Use 'purge' command to completely remove destroyed environments.")); } From 181db29821ec0e461f802f77911fcef2df7bbfde Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 17 Feb 2026 12:34:03 +0000 Subject: [PATCH 3/3] docs: [#359] update command documentation with real sample outputs - Add real sample outputs from full-stack environment to all command docs - Update validate.md with full-featured environment validation output - Update create.md with full-stack environment creation output - Update provision.md with connection details and DNS warning - Update configure.md with actual timing (38.2s) - Update release.md with successful release output - Update run.md with full service URLs (UDP/HTTP/HTTPS trackers, API, health, Grafana) - Update test.md with realistic HTTPS failure scenario - Update show.md with complete Running state (text and JSON outputs) - Update destroy.md with infrastructure teardown output - Update purge.md with successful purge output All examples now show real command execution with actual timings, IPs, and service URLs from a full-stack deployment with MySQL, multiple trackers, HTTPS (Caddy), Prometheus, Grafana, and backups. --- docs/user-guide/commands/configure.md | 18 +++--- docs/user-guide/commands/create.md | 18 +++--- docs/user-guide/commands/destroy.md | 25 ++++++-- docs/user-guide/commands/provision.md | 52 +++++++++-------- docs/user-guide/commands/purge.md | 10 ++++ docs/user-guide/commands/release.md | 12 +++- docs/user-guide/commands/run.md | 25 +++++--- docs/user-guide/commands/show.md | 83 +++++++++++++++++++-------- docs/user-guide/commands/test.md | 38 +++++++++--- docs/user-guide/commands/validate.md | 14 ++--- 10 files changed, 199 insertions(+), 96 deletions(-) diff --git a/docs/user-guide/commands/configure.md b/docs/user-guide/commands/configure.md index 58f56bf3d..09e3ceae3 100644 --- a/docs/user-guide/commands/configure.md +++ b/docs/user-guide/commands/configure.md @@ -47,16 +47,16 @@ When you configure an environment: ```bash # Configure the environment -torrust-tracker-deployer configure my-environment +torrust-tracker-deployer configure full-stack-docs # Output: -# ✓ Validating prerequisites... -# ✓ Running Ansible playbooks... -# ✓ Installing Docker... -# ✓ Installing Docker Compose... -# ✓ Configuring permissions... -# ✓ Verifying installation... -# ✓ Environment configured successfully +# ⏳ [1/3] Validating environment... +# ⏳ ✓ Environment name validated: full-stack-docs (took 0ms) +# ⏳ [2/3] Creating command handler... +# ⏳ ✓ Done (took 0ms) +# ⏳ [3/3] Configuring infrastructure... +# ⏳ ✓ Infrastructure configured (took 38.2s) +# ✅ Environment 'full-stack-docs' configured successfully ``` ### Configure multiple environments @@ -261,14 +261,12 @@ torrust-tracker-deployer configure my-environment The configure command runs these playbooks in order: 1. **install-docker.yml** - Installs Docker Engine - - Adds Docker GPG key - Adds Docker repository - Installs docker-ce, docker-ce-cli, containerd.io - Starts and enables Docker service 2. **install-docker-compose.yml** - Installs Docker Compose - - Downloads Docker Compose plugin - Installs to `/usr/local/lib/docker/cli-plugins/docker-compose` - Sets executable permissions diff --git a/docs/user-guide/commands/create.md b/docs/user-guide/commands/create.md index 9cd1e0264..4262b6287 100644 --- a/docs/user-guide/commands/create.md +++ b/docs/user-guide/commands/create.md @@ -248,21 +248,21 @@ torrust-tracker-deployer create environment --env-file config.json ```text ⏳ [1/3] Loading configuration... -⏳ → Loading configuration from 'config.json'... -⏳ ✓ Configuration loaded: my-env (took 2ms) +⏳ → Loading configuration from 'envs/full-stack-docs.json'... +⏳ ✓ Configuration loaded: full-stack-docs (took 0ms) ⏳ [2/3] Creating command handler... ⏳ ✓ Done (took 0ms) ⏳ [3/3] Creating environment... -⏳ → Creating environment 'my-env'... +⏳ → Creating environment 'full-stack-docs'... ⏳ → Validating configuration and creating environment... -⏳ ✓ Environment created: my-env (took 15ms) -✅ Environment 'my-env' created successfully +⏳ ✓ Environment created: full-stack-docs (took 1ms) +✅ Environment 'full-stack-docs' created successfully Environment Details: -1. Environment name: my-env -2. Instance name: torrust-tracker-vm-my-env -3. Data directory: ./data/my-env -4. Build directory: ./build/my-env +1. Environment name: full-stack-docs +2. Instance name: torrust-tracker-vm-full-stack-docs +3. Data directory: ./data/full-stack-docs +4. Build directory: ./build/full-stack-docs ``` **Features**: diff --git a/docs/user-guide/commands/destroy.md b/docs/user-guide/commands/destroy.md index 2c4eebea3..99357e1db 100644 --- a/docs/user-guide/commands/destroy.md +++ b/docs/user-guide/commands/destroy.md @@ -36,6 +36,21 @@ Destroy an environment: torrust-tracker-deployer destroy my-environment ``` +**Output**: + +```text +⏳ [1/3] Validating environment... +⏳ ✓ Environment name validated: full-stack-docs (took 0ms) +⏳ [2/3] Creating command handler... +⏳ ✓ Done (took 0ms) +⏳ [3/3] Tearing down infrastructure... +⏳ ✓ Infrastructure torn down (took 2.6s) +✅ Environment 'full-stack-docs' destroyed successfully + +💡 Local data preserved for debugging. To completely remove and reuse the name: + torrust-tracker-deployer purge full-stack-docs --force +``` + With verbose logging to see progress: ```bash @@ -344,18 +359,18 @@ name: Cleanup Test Environments on: schedule: - - cron: '0 2 * * *' # Run at 2 AM daily + - cron: "0 2 * * *" # Run at 2 AM daily jobs: cleanup: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - + - name: Setup tools run: | # Install OpenTofu, LXD, etc. - + - name: Destroy old environments run: | for env in test-1 test-2 staging-temp; do @@ -382,14 +397,14 @@ for env in "${ENVIRONMENTS_TO_DESTROY[@]}"; do echo "═══════════════════════════════════════════" echo "Destroying environment: $env" echo "═══════════════════════════════════════════" - + if torrust-tracker-deployer destroy "$env"; then echo "✓ Successfully destroyed $env" else echo "✗ Failed to destroy $env" # Continue with other environments fi - + echo "" done diff --git a/docs/user-guide/commands/provision.md b/docs/user-guide/commands/provision.md index c40919df2..7bd2a4092 100644 --- a/docs/user-guide/commands/provision.md +++ b/docs/user-guide/commands/provision.md @@ -35,34 +35,40 @@ Use the global `--output-format` flag to control the format. The default output format provides human-readable information with visual formatting: ```bash -torrust-tracker-deployer provision my-environment +torrust-tracker-deployer provision full-stack-docs ``` **Output**: ```text -✓ Rendering OpenTofu templates... -✓ Initializing infrastructure... -✓ Planning infrastructure changes... -✓ Applying infrastructure... -✓ Retrieving instance information... -✓ Instance IP: 10.140.190.42 -✓ Rendering Ansible templates... -✓ Waiting for SSH connectivity... -✓ Waiting for cloud-init completion... -✓ Environment provisioned successfully - -Provisioning Details: - 1. Environment name: my-environment - 2. Instance name: torrust-tracker-vm-my-environment - 3. Instance IP: 10.140.190.42 - 4. SSH credentials: - - Private key: /home/user/.ssh/id_rsa - - Public key: /home/user/.ssh/id_rsa.pub - - Username: torrust - - Port: 22 - 5. Provider: lxd - 6. Domains: (none) +⏳ [1/3] Validating environment... +⏳ ✓ Environment name validated: full-stack-docs (took 0ms) +⏳ [2/3] Creating command handler... +⏳ ✓ Done (took 0ms) +⏳ [3/3] Provisioning infrastructure... +⏳ ✓ Infrastructure provisioned (took 26.5s) +✅ Environment 'full-stack-docs' provisioned successfully + + +Instance Connection Details: + IP Address: 10.140.190.211 + SSH Port: 22 + SSH Private Key: /home/josecelano/Documents/git/committer/me/github/torrust/torrust-tracker-deployer-agent-01/fixtures/testing_rsa + SSH Username: torrust + +Connect using: + ssh -i /home/josecelano/Documents/git/committer/me/github/torrust/torrust-tracker-deployer-agent-01/fixtures/testing_rsa torrust@10.140.190.211 -p 22 + +⚠️ DNS Setup Required: + Your configuration uses custom domains. Remember to update your DNS records + to point your domains to the server IP: 10.140.190.211 + + Configured domains: + - tracker1.example.com + - tracker2.example.com + - api.example.com + - grafana.example.com + - health.example.com ``` **Features**: diff --git a/docs/user-guide/commands/purge.md b/docs/user-guide/commands/purge.md index 9aa5c526a..448fc13bb 100644 --- a/docs/user-guide/commands/purge.md +++ b/docs/user-guide/commands/purge.md @@ -61,6 +61,16 @@ For scripts and automation, use `--force` to skip the confirmation prompt: torrust-tracker-deployer purge my-environment --force ``` +**Output**: + +```text +⏳ [1/3] Validating environment... +⏳ ✓ Done (took 0ms) +⏳ [2/3] Purging local data... +⏳ ✓ Done (took 0ms) +✅ Environment 'full-stack-docs' purged successfully +``` + ### With Verbose Logging See detailed progress during purge: diff --git a/docs/user-guide/commands/release.md b/docs/user-guide/commands/release.md index 544c3eb3b..f713a55c5 100644 --- a/docs/user-guide/commands/release.md +++ b/docs/user-guide/commands/release.md @@ -96,7 +96,17 @@ If backup is enabled in your environment configuration, the release command also ```bash # Release after configuration -torrust-tracker-deployer release my-environment +torrust-tracker-deployer release full-stack-docs +``` + +**Output**: + +```text +⏳ [1/2] Validating environment... +⏳ ✓ Environment name validated: full-stack-docs (took 0ms) +⏳ [2/2] Releasing application... +⏳ ✓ Application released successfully (took 27.4s) +✅ Release command completed successfully for 'full-stack-docs' ``` ### Complete Workflow diff --git a/docs/user-guide/commands/run.md b/docs/user-guide/commands/run.md index 53dc4a430..91b328048 100644 --- a/docs/user-guide/commands/run.md +++ b/docs/user-guide/commands/run.md @@ -77,14 +77,23 @@ All services run inside a single `torrust/tracker:develop` Docker container. When the run command completes successfully, it displays service URLs for easy access: ```text -✓ Run command completed for 'my-environment' - -Service URLs: - API: http://192.168.1.100:1212 - HTTP Tracker: http://192.168.1.100:7070 - Health Check: http://192.168.1.100:1212/api/health_check - -Tip: Run 'torrust-tracker-deployer show my-environment' for full details +⏳ [1/2] Validating environment... +⏳ ✓ Environment name validated: full-stack-docs (took 0ms) +⏳ [2/2] Running application services... +⏳ ✓ Services started (took 39.9s) +✅ Run command completed for 'full-stack-docs' + +Services are now accessible: + Tracker (UDP): udp://10.140.190.211:6969/announce + Tracker (UDP): udp://10.140.190.211:6970/announce + Tracker (HTTP): https://tracker1.example.com/announce + Tracker (HTTP): https://tracker2.example.com/announce + API: https://api.example.com/api + Health Check: https://health.example.com/health_check + Grafana: https://grafana.example.com/ + +Note: HTTPS services require DNS configuration. See 'show' command for details. +Tip: Run 'torrust-tracker-deployer show full-stack-docs' for full details ``` **Notes**: diff --git a/docs/user-guide/commands/show.md b/docs/user-guide/commands/show.md index 51416b122..552d0d023 100644 --- a/docs/user-guide/commands/show.md +++ b/docs/user-guide/commands/show.md @@ -68,31 +68,46 @@ Next: Run 'configure my-environment' to install software When services have been deployed: ```text -Environment: my-environment +Environment: full-stack-docs State: Running Provider: LXD -Created: 2025-01-07 14:30:00 UTC +Created: 2026-02-17 12:10:49 UTC Infrastructure: - Instance IP: 10.140.190.171 + Instance IP: 10.140.190.211 SSH Port: 22 SSH User: torrust - SSH Key: ~/.ssh/torrust_deployer_key + SSH Key: /home/josecelano/Documents/git/committer/me/github/torrust/torrust-tracker-deployer-agent-01/fixtures/testing_rsa Connection: - ssh -i ~/.ssh/torrust_deployer_key torrust@10.140.190.171 + ssh -i /home/josecelano/Documents/git/committer/me/github/torrust/torrust-tracker-deployer-agent-01/fixtures/testing_rsa torrust@10.140.190.211 Tracker Services: UDP Trackers: - - udp://10.140.190.171:6969/announce - HTTP Trackers: - - http://10.140.190.171:7070/announce - API Endpoint: - - http://10.140.190.171:1212/api - Health Check: - - http://10.140.190.171:1313/health_check - -Tracker is running! Use the URLs above to connect. + - udp://10.140.190.211:6969/announce + - udp://10.140.190.211:6970/announce + HTTP Trackers (HTTPS via Caddy): + - https://tracker1.example.com/announce + - https://tracker2.example.com/announce + API Endpoint (HTTPS via Caddy): + - https://api.example.com/api + Health Check (HTTPS via Caddy): + - https://health.example.com/health_check + +Prometheus: + Internal only (localhost:9090) - not exposed externally + +Grafana (HTTPS via Caddy): + https://grafana.example.com/ + +Note: HTTPS services require domain-based access. For local domains (*.local), +add the following to your /etc/hosts file: + + 10.140.190.211 tracker1.example.com tracker2.example.com api.example.com grafana.example.com health.example.com + +Internal ports (7070, 7071, 1212, 3000, 1313) are not directly accessible when TLS is enabled. + +Services are running. Use 'test' to verify health. ``` ## Output Formats @@ -142,35 +157,53 @@ torrust-tracker-deployer show my-environment --output-format json ```json { - "name": "my-environment", + "name": "full-stack-docs", "state": "Running", "provider": "LXD", - "created_at": "2026-02-11T09:52:28.800407753Z", + "created_at": "2026-02-17T12:10:49.328958106Z", "infrastructure": { - "instance_ip": "10.140.190.36", + "instance_ip": "10.140.190.211", "ssh_port": 22, "ssh_user": "torrust", - "ssh_key_path": "/home/user/.ssh/torrust_key" + "ssh_key_path": "/home/josecelano/Documents/git/committer/me/github/torrust/torrust-tracker-deployer-agent-01/fixtures/testing_rsa" }, "services": { - "udp_trackers": ["udp://udp.tracker.local:6969/announce"], - "https_http_trackers": ["https://http.tracker.local/announce"], + "udp_trackers": [ + "udp://10.140.190.211:6969/announce", + "udp://10.140.190.211:6970/announce" + ], + "https_http_trackers": [ + "https://tracker1.example.com/announce", + "https://tracker2.example.com/announce" + ], "direct_http_trackers": [], "localhost_http_trackers": [], - "api_endpoint": "https://api.tracker.local/api", + "api_endpoint": "https://api.example.com/api", "api_uses_https": true, "api_is_localhost_only": false, - "health_check_url": "https://health.tracker.local/health_check", + "health_check_url": "https://health.example.com/health_check", "health_check_uses_https": true, "health_check_is_localhost_only": false, "tls_domains": [ { - "domain": "http.tracker.local", + "domain": "tracker1.example.com", "internal_port": 7070 }, { - "domain": "api.tracker.local", + "domain": "tracker2.example.com", + "internal_port": 7071 + }, + { + "domain": "api.example.com", "internal_port": 1212 + }, + { + "domain": "grafana.example.com", + "internal_port": 3000 + }, + { + "domain": "health.example.com", + "internal_port": 1313 } ] }, @@ -178,7 +211,7 @@ torrust-tracker-deployer show my-environment --output-format json "access_note": "Internal only (localhost:9090) - not exposed externally" }, "grafana": { - "url": "https://grafana.tracker.local/", + "url": "https://grafana.example.com/", "uses_https": true }, "state_name": "running" diff --git a/docs/user-guide/commands/test.md b/docs/user-guide/commands/test.md index 360f85f68..4abea1b4d 100644 --- a/docs/user-guide/commands/test.md +++ b/docs/user-guide/commands/test.md @@ -45,16 +45,38 @@ When you test an environment: ```bash # Test the environment -torrust-tracker-deployer test my-environment +torrust-tracker-deployer test full-stack-docs # Output: -# ✓ Validating environment state... -# ✓ Checking VM connectivity... -# ✓ Testing Docker installation... -# ✓ Testing Docker Compose... -# ✓ Verifying user permissions... -# ✓ Running infrastructure tests... -# ✓ All tests passed +# ⏳ [1/3] Validating environment... +# ⏳ ✓ Environment name validated: full-stack-docs (took 0ms) +# ⏳ [2/3] Creating command handler... +# ⏳ ✓ Done (took 0ms) +# ⏳ [3/3] Testing infrastructure... +# ❌ Test command failed: Validation failed for environment 'full-stack-docs': Remote action failed: Action 'running-services-validation' validation failed: HTTPS request to 'https://api.example.com/api/health_check' failed: error sending request for url (https://api.example.com/api/health_check). Check that Caddy is running and port 443 is open. Domain 'api.example.com' was resolved to 10.140.190.211 for testing. +# Tip: Check logs and try running with --log-output file-and-stderr for more details +# +# For detailed troubleshooting: +# Validation Failed - Detailed Troubleshooting: +# +# 1. Check validation logs for specific failure: +# - Re-run with verbose logging: +# torrust-tracker-deployer test --log-output file-and-stderr +# +# 2. Common validation failures: +# - Cloud-init not completed: Wait for instance initialization +# - Docker not installed: Run configure command +# - Docker Compose not installed: Run configure command +# +# 3. Remediation steps: +# - If cloud-init failed: Destroy and re-provision +# - If Docker/Compose missing: Run configure command +# torrust-tracker-deployer configure +# +# 4. Check instance status: +# - Verify instance is running +# - Check SSH connectivity +# - Review system logs on the instance ``` ### Complete workflow diff --git a/docs/user-guide/commands/validate.md b/docs/user-guide/commands/validate.md index d8cdda80d..2d377bdad 100644 --- a/docs/user-guide/commands/validate.md +++ b/docs/user-guide/commands/validate.md @@ -66,15 +66,15 @@ The validate command performs comprehensive validation of environment configurat ⏳ [3/3] Validating configuration fields... ⏳ ✓ Field validation passed (took 0ms) -✅ Configuration file 'envs/my-environment.json' is valid +✅ Configuration file 'envs/full-stack-docs.json' is valid Environment Details: - • Name: my-environment - • Provider: lxd - • Prometheus: Enabled - • Grafana: Enabled - • HTTPS: Disabled - • Backups: Enabled +• Name: full-stack-docs +• Provider: lxd +• Prometheus: Enabled +• Grafana: Enabled +• HTTPS: Enabled +• Backups: Enabled ``` ### Error Output Examples