diff --git a/docs/issues/367-add-verbosity-levels-to-release-command.md b/docs/issues/367-add-verbosity-levels-to-release-command.md index 52a2f0a3..bab54779 100644 --- a/docs/issues/367-add-verbosity-levels-to-release-command.md +++ b/docs/issues/367-add-verbosity-levels-to-release-command.md @@ -12,20 +12,20 @@ - [Generic Command Progress Listener for Verbosity](./drafts/generic-command-progress-listener-for-verbosity.md) — architectural design for the `CommandProgressListener` trait - [Progress Reporting in Application Layer](../features/progress-reporting-in-application-layer/README.md) -**Status**: 🔜 **NOT STARTED** — Ready for implementation +**Status**: ✅ **COMPLETED** — Implementation finished -**Branch**: TBD +**Branch**: `367-add-verbosity-levels-to-release-command` **PR**: TBD ## Current Implementation Status -**Pending Implementation**: +**Implemented**: -- [ ] **Normal (default)**: Show 2 main workflow phases (validate + release) -- [ ] **Verbose (`-v`)**: Show all 7 service-specific release steps -- [ ] **VeryVerbose (`-vv`)**: Show detail messages (files deployed, templates rendered, paths) -- [ ] **Debug (`-vvv`)**: Show debug messages (Ansible commands, working directories, full outputs) -- [ ] **Documentation**: User guide update with verbosity examples and patterns +- [x] **Normal (default)**: Show 2 main workflow phases (validate + release) +- [x] **Verbose (`-v`)**: Show all 7 service-specific release steps +- [x] **VeryVerbose (`-vv`)**: Show detail messages (files deployed, templates rendered, paths) +- [x] **Debug (`-vvv`)**: Show debug messages (Ansible commands, working directories, full outputs) +- [x] **Documentation**: User guide updated with verbosity examples and patterns ## Overview @@ -35,13 +35,13 @@ Add graduated verbosity levels (`-v`, `-vv`, `-vvv`) to the `release` command to ## Goals -- [ ] Reuse `CommandProgressListener` infrastructure from provision/configure commands -- [ ] Apply same four verbosity levels to release command -- [ ] Show service-specific release steps, template rendering, file deployments at different detail levels -- [ ] Maintain backward compatibility (default = Normal level) -- [ ] Keep user output completely separate from tracing logs -- [ ] Update user documentation with verbosity examples -- [ ] Help text examples already present (global flags) +- [x] Reuse `CommandProgressListener` infrastructure from provision/configure commands +- [x] Apply same four verbosity levels to release command +- [x] Show service-specific release steps, template rendering, file deployments at different detail levels +- [x] Maintain backward compatibility (default = Normal level) +- [x] Keep user output completely separate from tracing logs +- [x] Update user documentation with verbosity examples +- [x] Help text examples already present (global flags) ## 🏗️ Architecture Requirements @@ -73,29 +73,29 @@ The following components **already exist** from previous verbosity implementatio **What's needed for release command**: -- [ ] Pass `CommandProgressListener` to release command handler -- [ ] Add `on_step_started()` calls for all 7 service-specific release steps -- [ ] Add `on_detail()` calls for template rendering, file deployments, and service operations -- [ ] Add `on_debug()` calls for Ansible commands, working directories, full outputs -- [ ] Update user guide documentation -- [ ] Verify help text shows verbosity options (already present from global flags) +- [x] Pass `CommandProgressListener` to release command handler +- [x] Add `on_step_started()` calls for all 7 service-specific release steps +- [x] Add `on_detail()` calls for template rendering, file deployments, and service operations +- [x] Add `on_debug()` calls for Ansible commands, working directories, full outputs +- [x] Update user guide documentation +- [x] Verify help text shows verbosity options (already present from global flags) ### Module Structure Requirements -- [ ] Follow same pattern as provision/configure commands (reference implementations) -- [ ] Handler emits step-level progress events -- [ ] Workflow coordinates service steps and emits progress for each service -- [ ] Service steps emit detail and debug events -- [ ] Infrastructure layer (Ansible, template rendering) remains unaware of listener +- [x] Follow same pattern as provision/configure commands (reference implementations) +- [x] Handler emits step-level progress events +- [x] Workflow coordinates service steps and emits progress for each service +- [x] Service steps emit detail and debug events +- [x] Infrastructure layer (Ansible, template rendering) remains unaware of listener ### Architectural Constraints -- [ ] Verbosity flags control **only UserOutput** (user-facing messages) -- [ ] **Do not** mix verbosity with tracing logs (logs use `RUST_LOG`) -- [ ] Follow separation documented in [user-output-vs-logging-separation.md](../research/UX/user-output-vs-logging-separation.md) -- [ ] Maintain channel separation (stdout for results, stderr for progress) -- [ ] Backward compatible (default = Normal level, existing output unchanged) -- [ ] Reuse `CommandProgressListener` trait (do not create command-specific variants) +- [x] Verbosity flags control **only UserOutput** (user-facing messages) +- [x] **Do not** mix verbosity with tracing logs (logs use `RUST_LOG`) +- [x] Follow separation documented in [user-output-vs-logging-separation.md](../research/UX/user-output-vs-logging-separation.md) +- [x] Maintain channel separation (stdout for results, stderr for progress) +- [x] Backward compatible (default = Normal level, existing output unchanged) +- [x] Reuse `CommandProgressListener` trait (do not create command-specific variants) ### Anti-Patterns to Avoid @@ -266,12 +266,12 @@ torrust-tracker-deployer release my-env -vvv **Tasks**: -- [ ] Task 1.1: Update `ReleaseCommandHandler::execute()` to accept optional `&dyn CommandProgressListener` -- [ ] Task 1.2: Add `TOTAL_RELEASE_STEPS` constant (7 services) -- [ ] Task 1.3: Add `on_step_started()` calls before each service release in `workflow::execute()` -- [ ] Task 1.4: Pass listener from controller to handler -- [ ] Task 1.5: Update E2E tests to pass `None` for listener (backward compatibility) -- [ ] Task 1.6: Test Normal and Verbose levels work correctly +- [x] Task 1.1: Update `ReleaseCommandHandler::execute()` to accept optional `&dyn CommandProgressListener` +- [x] Task 1.2: Add `TOTAL_RELEASE_STEPS` constant (7 services) +- [x] Task 1.3: Add `on_step_started()` calls before each service release in `workflow::execute()` +- [x] Task 1.4: Pass listener from controller to handler +- [x] Task 1.5: Update E2E tests to pass `None` for listener (backward compatibility) +- [x] Task 1.6: Test Normal and Verbose levels work correctly **Rationale**: Matches the pattern established in provision and configure commands. Handler orchestrates high-level workflow, emitting progress for each major service step. @@ -294,20 +294,20 @@ torrust-tracker-deployer release my-env -vvv **Tasks**: -- [ ] Task 2.1: Update all 7 service release functions to accept optional `&dyn CommandProgressListener` -- [ ] Task 2.2: Add `on_detail()` calls for: +- [x] Task 2.1: Update all 7 service release functions to accept optional `&dyn CommandProgressListener` +- [x] Task 2.2: Add `on_detail()` calls for: - Storage directory creation - Database initialization - Template rendering operations - File deployment paths - Service-specific operations -- [ ] Task 2.3: Add `on_debug()` calls for: +- [x] Task 2.3: Add `on_debug()` calls for: - Ansible command execution - Working directories - Template source/output paths - Playbook names -- [ ] Task 2.4: Pass listener from workflow to all service steps -- [ ] Task 2.5: Test VeryVerbose and Debug levels work correctly +- [x] Task 2.4: Pass listener from workflow to all service steps +- [x] Task 2.5: Test VeryVerbose and Debug levels work correctly **Rationale**: Service steps are the natural place to report detailed progress. They know what files are being rendered, where they're deployed, and what Ansible commands execute. Following the same pattern as configure command. @@ -324,12 +324,12 @@ torrust-tracker-deployer release my-env -vvv **Tasks**: -- [ ] Task 3.1: Add "Verbosity Levels" section to release.md -- [ ] Task 3.2: Include examples for all 4 verbosity levels (Normal, Verbose, VeryVerbose, Debug) -- [ ] Task 3.3: Document use cases for each level -- [ ] Task 3.4: Add examples showing combined usage with other flags -- [ ] Task 3.5: Verify help text includes verbosity examples (already present from global flags) -- [ ] Task 3.6: Update this issue spec with implementation status +- [x] Task 3.1: Add "Verbosity Levels" section to release.md +- [x] Task 3.2: Include examples for all 4 verbosity levels (Normal, Verbose, VeryVerbose, Debug) +- [x] Task 3.3: Document use cases for each level +- [x] Task 3.4: Add examples showing combined usage with other flags +- [x] Task 3.5: Verify help text includes verbosity examples (already present from global flags) +- [x] Task 3.6: Update this issue spec with implementation status **Rationale**: Same validation as provision/configure commands. Ensure clean, readable output and comprehensive documentation. @@ -343,38 +343,38 @@ torrust-tracker-deployer release my-env -vvv **Quality Checks**: -- [ ] Pre-commit checks pass: `./scripts/pre-commit.sh` -- [ ] No unused dependencies: `cargo machete` -- [ ] All existing tests pass -- [ ] No clippy warnings +- [x] Pre-commit checks pass: `./scripts/pre-commit.sh` +- [x] No unused dependencies: `cargo machete` +- [x] All existing tests pass +- [x] No clippy warnings **Task-Specific Criteria**: -- [ ] Release command accepts `-v`, `-vv`, `-vvv` flags (global flags, already works) -- [ ] Default behavior (no flags) remains unchanged from current output -- [ ] Verbose level (`-v`) shows all 7 service-specific release steps -- [ ] VeryVerbose level (`-vv`) shows template rendering, file deployments, storage operations -- [ ] Debug level (`-vvv`) shows Ansible commands, working directories, full paths -- [ ] User output stays completely separate from tracing logs -- [ ] `RUST_LOG` continues to control logging independently -- [ ] Help text clearly explains verbosity levels (verified in global flags) -- [ ] Output remains clean and readable at all verbosity levels -- [ ] Channel separation maintained (stdout for results, stderr for progress) -- [ ] Pattern matches provision/configure command implementations (consistency) +- [x] Release command accepts `-v`, `-vv`, `-vvv` flags (global flags, already works) +- [x] Default behavior (no flags) remains unchanged from current output +- [x] Verbose level (`-v`) shows all 7 service-specific release steps +- [x] VeryVerbose level (`-vv`) shows template rendering, file deployments, storage operations +- [x] Debug level (`-vvv`) shows Ansible commands, working directories, full paths +- [x] User output stays completely separate from tracing logs +- [x] `RUST_LOG` continues to control logging independently +- [x] Help text clearly explains verbosity levels (verified in global flags) +- [x] Output remains clean and readable at all verbosity levels +- [x] Channel separation maintained (stdout for results, stderr for progress) +- [x] Pattern matches provision/configure command implementations (consistency) **Documentation Criteria**: -- [ ] User guide (`docs/user-guide/commands/release.md`) updated with verbosity section -- [ ] Examples provided for all 4 verbosity levels -- [ ] Symbol legend explained (⏳ ✅ 📋 🔍) -- [ ] Use cases documented for each verbosity level -- [ ] Combined flag usage examples provided +- [x] User guide (`docs/user-guide/commands/release.md`) updated with verbosity section +- [x] Examples provided for all 4 verbosity levels +- [x] Symbol legend explained (⏳ ✅ 📋 🔍) +- [x] Use cases documented for each verbosity level +- [x] Combined flag usage examples provided **Out of Scope**: -- [ ] Quiet mode (`-q`) - defer to future work -- [ ] Silent mode - defer to future work -- [ ] Per-step timing breakdown - not needed, total duration is sufficient +- [x] Quiet mode (`-q`) - defer to future work +- [x] Silent mode - defer to future work +- [x] Per-step timing breakdown - not needed, total duration is sufficient ## Related Documentation diff --git a/docs/user-guide/commands/release.md b/docs/user-guide/commands/release.md index f713a55c..b4ed0cbb 100644 --- a/docs/user-guide/commands/release.md +++ b/docs/user-guide/commands/release.md @@ -130,6 +130,168 @@ torrust-tracker-deployer release my-environment torrust-tracker-deployer run my-environment ``` +## Verbosity Levels + +The release command supports multiple verbosity levels to control the amount of progress detail displayed: + +### Default (Normal) - Essential Progress Only + +Shows only the essential progress and results: + +```bash +torrust-tracker-deployer release my-environment +``` + +**Output**: + +```text +⏳ [1/2] Validating environment... +⏳ ✓ Environment name validated: my-environment (took 0ms) +⏳ [2/2] Releasing application... +⏳ ✓ Application released successfully (took 45.8s) +✅ Release command completed successfully for 'my-environment' +``` + +### Verbose (`-v`) - Show Service Release Steps + +Shows all 7 service-specific release steps: + +```bash +torrust-tracker-deployer release my-environment -v +``` + +**Output**: + +```text +⏳ [1/2] Validating environment... +⏳ ✓ Environment name validated: my-environment (took 0ms) +⏳ [2/2] Releasing application... +📋 [Step 1/7] Releasing Tracker service... +📋 [Step 2/7] Releasing Prometheus service... +📋 [Step 3/7] Releasing Grafana service... +📋 [Step 4/7] Releasing MySQL service... +📋 [Step 5/7] Releasing Backup service... +📋 [Step 6/7] Releasing Caddy service... +📋 [Step 7/7] Deploying Docker Compose configuration... +⏳ ✓ Application released successfully (took 43.2s) +✅ Release command completed successfully for 'my-environment' +``` + +**Use Case**: When you want visibility into which service is being deployed. + +### Very Verbose (`-vv`) - Show Detailed Operations + +Shows template rendering, file paths, and deployment details: + +```bash +torrust-tracker-deployer release my-environment -vv +``` + +**Output** (excerpt): + +```text +⏳ [1/2] Validating environment... +⏳ ✓ Environment name validated: my-environment (took 0ms) +⏳ [2/2] Releasing application... +📋 [Step 1/7] Releasing Tracker service... +📋 → Creating storage directories: /opt/torrust/storage/tracker/{lib,log,etc} +📋 → Initializing database: tracker.db +📋 → Rendering tracker.toml from template +📋 → Deploying config to /opt/torrust/storage/tracker/etc/tracker.toml +📋 [Step 2/7] Releasing Prometheus service... +📋 → Creating storage directories: /opt/torrust/storage/prometheus/etc +📋 → Rendering prometheus.yml from template +📋 → Deploying config to /opt/torrust/storage/prometheus/etc/prometheus.yml +📋 [Step 3/7] Releasing Grafana service... +📋 → Creating storage directories: /opt/torrust/storage/grafana/{data,provisioning} +📋 → Rendering Grafana provisioning files (datasources, dashboards) +📋 → Deploying provisioning to /opt/torrust/storage/grafana/provisioning +📋 [Step 7/7] Deploying Docker Compose configuration... +📋 → Rendering docker-compose.yml and .env from templates +📋 → Deploying docker-compose.yml and .env to /opt/torrust +⏳ ✓ Application released successfully (took 43.5s) +✅ Release command completed successfully for 'my-environment' +``` + +**Use Case**: Troubleshooting release issues or verifying what files are being deployed where. + +### Debug (`-vvv`) - Show Technical Details + +Shows Ansible commands, working directories, and full execution details: + +```bash +torrust-tracker-deployer release my-environment -vvv +``` + +**Output** (excerpt): + +```text +⏳ [1/2] Validating environment... +⏳ ✓ Environment name validated: my-environment (took 0ms) +⏳ [2/2] Releasing application... +📋 [Step 1/7] Releasing Tracker service... +🔍 → Ansible working directory: ./build/my-environment/ansible +🔍 → Executing playbook: ansible-playbook create-tracker-storage.yml +📋 → Creating storage directories: /opt/torrust/storage/tracker/{lib,log,etc} +🔍 → Executing playbook: ansible-playbook init-tracker-database.yml +📋 → Initializing database: tracker.db +🔍 → Template source: ./data/my-environment/templates/tracker/ +📋 → Rendering tracker.toml from template +🔍 → Template output: ./build/my-environment/tracker +🔍 → Executing playbook: ansible-playbook deploy-tracker-config.yml +📋 → Deploying config to /opt/torrust/storage/tracker/etc/tracker.toml +📋 [Step 7/7] Deploying Docker Compose configuration... +🔍 → Template source: ./data/my-environment/templates/docker-compose/ +📋 → Rendering docker-compose.yml and .env from templates +🔍 → Template output: ./build/my-environment/docker-compose +🔍 → Ansible working directory: ./build/my-environment/ansible +🔍 → Executing playbook: ansible-playbook deploy-compose-files.yml +📋 → Deploying docker-compose.yml and .env to /opt/torrust +⏳ ✓ Application released successfully (took 43.8s) +✅ Release command completed successfully for 'my-environment' +``` + +**Use Case**: Deep troubleshooting, debugging, or when you need to understand exactly what commands are being executed. + +### Symbol Legend + +| Symbol | Meaning | Verbosity Level | +| ------ | -------------------------------- | --------------- | +| ⏳ | Operation in progress | Normal+ | +| ✅ | Operation completed successfully | Normal+ | +| 📋 | Detailed contextual information | VeryVerbose | +| 🔍 | Technical implementation detail | Debug | + +### Combining with Other Options + +Verbosity flags can be combined with other command options: + +```bash +# Very verbose release with trace logging +RUST_LOG=trace torrust-tracker-deployer release my-environment -vv +``` + +**Note**: Verbosity flags (`-v`, `-vv`, `-vvv`) control user-facing progress output, while `RUST_LOG` controls internal application logging for debugging purposes. + +```bash +# 1. Create environment +torrust-tracker-deployer create template --provider lxd > my-env.json +# Edit my-env.json with your settings +torrust-tracker-deployer create environment --env-file my-env.json + +# 2. Provision infrastructure +torrust-tracker-deployer provision my-environment + +# 3. Configure system +torrust-tracker-deployer configure my-environment + +# 4. Release application +torrust-tracker-deployer release my-environment + +# 5. Start services (next step) +torrust-tracker-deployer run my-environment +``` + ## What Gets Configured ### Tracker Configuration (`tracker.toml`) diff --git a/src/application/command_handlers/release/handler.rs b/src/application/command_handlers/release/handler.rs index 9d671c5a..a702b63b 100644 --- a/src/application/command_handlers/release/handler.rs +++ b/src/application/command_handlers/release/handler.rs @@ -6,12 +6,19 @@ use tracing::{error, info, instrument}; use super::errors::ReleaseCommandHandlerError; use super::workflow; +use crate::application::traits::CommandProgressListener; use crate::domain::environment::repository::{EnvironmentRepository, TypedEnvironmentRepository}; use crate::domain::environment::state::{ReleaseFailureContext, ReleaseStep}; use crate::domain::environment::{Configured, Environment, Released, Releasing}; use crate::domain::EnvironmentName; use crate::shared::error::Traceable; +/// Total number of steps in the release workflow. +/// +/// This constant is used for progress reporting via `CommandProgressListener` +/// to display step progress like "[Step 1/7] Releasing Tracker service...". +pub(super) const TOTAL_RELEASE_STEPS: usize = 7; + /// `ReleaseCommandHandler` orchestrates the software release workflow /// /// The `ReleaseCommandHandler` orchestrates the software release workflow to @@ -63,6 +70,7 @@ impl ReleaseCommandHandler { /// # Arguments /// /// * `env_name` - The name of the environment to release to + /// * `listener` - Optional progress listener for step-level reporting /// /// # Returns /// @@ -87,6 +95,7 @@ impl ReleaseCommandHandler { pub async fn execute( &self, env_name: &EnvironmentName, + listener: Option<&dyn CommandProgressListener>, ) -> Result, ReleaseCommandHandlerError> { let environment = self.load_configured_environment(env_name)?; @@ -119,7 +128,7 @@ impl ReleaseCommandHandler { "Releasing state persisted. Executing release steps." ); - match workflow::execute(&releasing_env).await { + match workflow::execute(&releasing_env, listener).await { Ok(released) => { info!( command = "release", diff --git a/src/application/command_handlers/release/steps/backup.rs b/src/application/command_handlers/release/steps/backup.rs index e5465c0d..14b57683 100644 --- a/src/application/command_handlers/release/steps/backup.rs +++ b/src/application/command_handlers/release/steps/backup.rs @@ -14,6 +14,7 @@ use crate::application::command_handlers::release::errors::ReleaseCommandHandler use crate::application::steps::application::{CreateBackupStorageStep, DeployBackupConfigStep}; use crate::application::steps::rendering::RenderBackupTemplatesStep; use crate::application::steps::system::InstallBackupCrontabStep; +use crate::application::traits::CommandProgressListener; use crate::domain::environment::state::ReleaseStep; use crate::domain::environment::{Environment, Releasing}; @@ -26,6 +27,11 @@ use crate::domain::environment::{Environment, Releasing}; /// /// The function returns early if backup is not configured in the environment. /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns `ReleaseCommandHandlerError` if: @@ -35,6 +41,7 @@ use crate::domain::environment::{Environment, Releasing}; #[allow(clippy::result_large_err)] pub async fn release( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { // Check if backup is configured if environment.context().user_inputs.backup().is_none() { @@ -47,25 +54,38 @@ pub async fn release( return Ok(()); } - render_templates(environment).await?; - create_storage(environment)?; - deploy_config_to_remote(environment)?; - install_crontab(environment)?; + render_templates(environment, listener).await?; + create_storage(environment, listener)?; + deploy_config_to_remote(environment, listener)?; + install_crontab(environment, listener)?; Ok(()) } /// Render backup configuration templates to the build directory /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, `ReleaseStep::RenderBackupTemplates`) if rendering fails #[allow(clippy::result_large_err)] async fn render_templates( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { let current_step = ReleaseStep::RenderBackupTemplates; + if let Some(l) = listener { + l.on_debug(&format!( + "Template source: {}/backup/", + environment.templates_dir().display() + )); + } + let step = RenderBackupTemplatesStep::new( Arc::new(environment.clone()), environment.templates_dir(), @@ -83,6 +103,10 @@ async fn render_templates( ) })?; + if let Some(l) = listener { + l.on_detail("Rendering backup scripts and configuration from templates"); + } + info!( command = "release", step = %current_step, @@ -94,15 +118,29 @@ async fn render_templates( /// Create backup storage directories on the remote host /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, `ReleaseStep::CreateBackupStorage`) if storage creation fails #[allow(clippy::result_large_err)] fn create_storage( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { let current_step = ReleaseStep::CreateBackupStorage; + if let Some(l) = listener { + l.on_debug(&format!( + "Ansible working directory: {}", + environment.ansible_build_dir().display() + )); + l.on_debug("Executing playbook: ansible-playbook create-backup-storage.yml"); + } + CreateBackupStorageStep::new(ansible_client(environment)) .execute() .map_err(|e| { @@ -116,6 +154,10 @@ fn create_storage( ) })?; + if let Some(l) = listener { + l.on_detail("Creating storage directories: /opt/torrust/backup/{scripts,data,logs}"); + } + info!( command = "release", step = %current_step, @@ -127,15 +169,25 @@ fn create_storage( /// Deploy backup configuration files to the remote host via Ansible /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, `ReleaseStep::DeployBackupConfigToRemote`) if deployment fails #[allow(clippy::result_large_err)] fn deploy_config_to_remote( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { let current_step = ReleaseStep::DeployBackupConfigToRemote; + if let Some(l) = listener { + l.on_debug("Executing playbook: ansible-playbook deploy-backup-config.yml"); + } + DeployBackupConfigStep::new(ansible_client(environment)) .execute() .map_err(|e| { @@ -149,6 +201,10 @@ fn deploy_config_to_remote( ) })?; + if let Some(l) = listener { + l.on_detail("Deploying backup scripts to /opt/torrust/backup/scripts"); + } + info!( command = "release", step = %current_step, @@ -163,15 +219,25 @@ fn deploy_config_to_remote( /// This installs the cron job that will execute backups on the configured schedule. /// The cron daemon is always running, so the job will automatically execute on schedule. /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, `ReleaseStep::InstallBackupCrontab`) if installation fails #[allow(clippy::result_large_err)] fn install_crontab( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { let current_step = ReleaseStep::InstallBackupCrontab; + if let Some(l) = listener { + l.on_debug("Executing playbook: ansible-playbook install-backup-crontab.yml"); + } + InstallBackupCrontabStep::new(ansible_client(environment)) .execute() .map_err(|e| { @@ -185,6 +251,10 @@ fn install_crontab( ) })?; + if let Some(l) = listener { + l.on_detail("Installing crontab for automated backups"); + } + info!( command = "release", step = %current_step, diff --git a/src/application/command_handlers/release/steps/caddy.rs b/src/application/command_handlers/release/steps/caddy.rs index 3bb4345d..fe59302d 100644 --- a/src/application/command_handlers/release/steps/caddy.rs +++ b/src/application/command_handlers/release/steps/caddy.rs @@ -15,6 +15,7 @@ use crate::application::command_handlers::common::StepResult; use crate::application::command_handlers::release::errors::ReleaseCommandHandlerError; use crate::application::steps::application::DeployCaddyConfigStep; use crate::application::steps::rendering::RenderCaddyTemplatesStep; +use crate::application::traits::CommandProgressListener; use crate::domain::environment::state::ReleaseStep; use crate::domain::environment::{Environment, Releasing}; use crate::shared::clock::SystemClock; @@ -27,12 +28,18 @@ use crate::shared::clock::SystemClock; /// /// If HTTPS is not configured, all steps are skipped. /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, step) if any Caddy step fails #[allow(clippy::result_large_err)] pub fn release( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { // Check if HTTPS is configured if environment.context().user_inputs.https().is_none() { @@ -45,22 +52,35 @@ pub fn release( return Ok(()); } - render_templates(environment)?; - deploy_config_to_remote(environment)?; + render_templates(environment, listener)?; + deploy_config_to_remote(environment, listener)?; Ok(()) } /// Render Caddy configuration templates /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, `ReleaseStep::RenderCaddyTemplates`) if rendering fails #[allow(clippy::result_large_err)] fn render_templates( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { let current_step = ReleaseStep::RenderCaddyTemplates; + if let Some(l) = listener { + l.on_debug(&format!( + "Template source: {}/caddy/", + environment.templates_dir().display() + )); + } + let clock = Arc::new(SystemClock); let step = RenderCaddyTemplatesStep::new( Arc::new(environment.clone()), @@ -79,6 +99,10 @@ fn render_templates( ) })?; + if let Some(l) = listener { + l.on_detail("Rendering Caddyfile from template"); + } + info!( command = "release", step = %current_step, @@ -90,15 +114,25 @@ fn render_templates( /// Deploy Caddy configuration to the remote host /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, `ReleaseStep::DeployCaddyConfigToRemote`) if deployment fails #[allow(clippy::result_large_err)] fn deploy_config_to_remote( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { let current_step = ReleaseStep::DeployCaddyConfigToRemote; + if let Some(l) = listener { + l.on_debug("Executing playbook: ansible-playbook deploy-caddy-config.yml"); + } + DeployCaddyConfigStep::new(ansible_client(environment)) .execute() .map_err(|e| { @@ -111,6 +145,10 @@ fn deploy_config_to_remote( ) })?; + if let Some(l) = listener { + l.on_detail("Deploying Caddyfile to /opt/torrust/Caddyfile"); + } + info!( command = "release", step = %current_step, diff --git a/src/application/command_handlers/release/steps/compose.rs b/src/application/command_handlers/release/steps/compose.rs index 7a47a68c..67dc1960 100644 --- a/src/application/command_handlers/release/steps/compose.rs +++ b/src/application/command_handlers/release/steps/compose.rs @@ -13,6 +13,7 @@ use crate::adapters::ansible::AnsibleClient; use crate::application::command_handlers::common::StepResult; use crate::application::command_handlers::release::errors::ReleaseCommandHandlerError; use crate::application::steps::{DeployComposeFilesStep, RenderDockerComposeTemplatesStep}; +use crate::application::traits::CommandProgressListener; use crate::domain::environment::state::ReleaseStep; use crate::domain::environment::{Environment, Releasing}; use crate::shared::clock::SystemClock; @@ -23,27 +24,46 @@ use crate::shared::clock::SystemClock; /// 1. Render Docker Compose templates /// 2. Deploy compose files to remote /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, step) if any Docker Compose step fails pub async fn release( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { - let compose_build_dir = render_templates(environment).await?; - deploy_files_to_remote(environment, &compose_build_dir)?; + let compose_build_dir = render_templates(environment, listener).await?; + deploy_files_to_remote(environment, &compose_build_dir, listener)?; Ok(()) } /// Render Docker Compose templates to the build directory /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, `ReleaseStep::RenderDockerComposeTemplates`) if rendering fails async fn render_templates( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult { let current_step = ReleaseStep::RenderDockerComposeTemplates; + if let Some(l) = listener { + l.on_debug(&format!( + "Template source: {}/docker-compose/", + environment.templates_dir().display() + )); + } + let clock = Arc::new(SystemClock); let step = RenderDockerComposeTemplatesStep::new( Arc::new(environment.clone()), @@ -62,6 +82,11 @@ async fn render_templates( ) })?; + if let Some(l) = listener { + l.on_detail("Rendering docker-compose.yml and .env from templates"); + l.on_debug(&format!("Template output: {}", compose_build_dir.display())); + } + info!( command = "release", compose_build_dir = %compose_build_dir.display(), @@ -77,6 +102,7 @@ async fn render_templates( /// /// * `environment` - The environment in Releasing state /// * `compose_build_dir` - Path to the rendered compose files +/// * `listener` - Optional progress listener for detail and debug reporting /// /// # Errors /// @@ -85,9 +111,18 @@ async fn render_templates( fn deploy_files_to_remote( environment: &Environment, compose_build_dir: &Path, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { let current_step = ReleaseStep::DeployComposeFilesToRemote; + if let Some(l) = listener { + l.on_debug(&format!( + "Ansible working directory: {}", + environment.ansible_build_dir().display() + )); + l.on_debug("Executing playbook: ansible-playbook deploy-compose-files.yml"); + } + let ansible_client = Arc::new(AnsibleClient::new(environment.ansible_build_dir())); let step = DeployComposeFilesStep::new(ansible_client, compose_build_dir.to_path_buf()); @@ -101,6 +136,10 @@ fn deploy_files_to_remote( ) })?; + if let Some(l) = listener { + l.on_detail("Deploying docker-compose.yml and .env to /opt/torrust"); + } + info!( command = "release", compose_build_dir = %compose_build_dir.display(), diff --git a/src/application/command_handlers/release/steps/grafana.rs b/src/application/command_handlers/release/steps/grafana.rs index 11485f25..2857f6c0 100644 --- a/src/application/command_handlers/release/steps/grafana.rs +++ b/src/application/command_handlers/release/steps/grafana.rs @@ -19,6 +19,7 @@ use crate::application::steps::application::{ CreateGrafanaStorageStep, DeployGrafanaProvisioningStep, }; use crate::application::steps::rendering::RenderGrafanaTemplatesStep; +use crate::application::traits::CommandProgressListener; use crate::domain::environment::state::ReleaseStep; use crate::domain::environment::{Environment, Releasing}; use crate::shared::clock::SystemClock; @@ -33,12 +34,18 @@ use crate::shared::clock::SystemClock; /// If Grafana is not configured, all steps are skipped. /// Provisioning steps are skipped if Prometheus is not configured. /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, step) if any Grafana step fails #[allow(clippy::result_large_err)] pub fn release( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { // Check if Grafana is configured if environment.context().user_inputs.grafana().is_none() { @@ -51,7 +58,7 @@ pub fn release( return Ok(()); } - create_storage(environment)?; + create_storage(environment, listener)?; // Provisioning requires Prometheus for datasource configuration if environment.context().user_inputs.prometheus().is_none() { @@ -64,22 +71,36 @@ pub fn release( return Ok(()); } - render_templates(environment)?; - deploy_provisioning_to_remote(environment)?; + render_templates(environment, listener)?; + deploy_provisioning_to_remote(environment, listener)?; Ok(()) } /// Create Grafana storage directories on the remote host /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, `ReleaseStep::CreateGrafanaStorage`) if creation fails #[allow(clippy::result_large_err)] fn create_storage( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { let current_step = ReleaseStep::CreateGrafanaStorage; + if let Some(l) = listener { + l.on_debug(&format!( + "Ansible working directory: {}", + environment.ansible_build_dir().display() + )); + l.on_debug("Executing playbook: ansible-playbook create-grafana-storage.yml"); + } + CreateGrafanaStorageStep::new(ansible_client(environment)) .execute() .map_err(|e| { @@ -92,6 +113,12 @@ fn create_storage( ) })?; + if let Some(l) = listener { + l.on_detail( + "Creating storage directories: /opt/torrust/storage/grafana/{data,provisioning}", + ); + } + info!( command = "release", step = %current_step, @@ -103,15 +130,28 @@ fn create_storage( /// Render Grafana provisioning templates /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, `ReleaseStep::RenderGrafanaTemplates`) if rendering fails #[allow(clippy::result_large_err)] fn render_templates( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { let current_step = ReleaseStep::RenderGrafanaTemplates; + if let Some(l) = listener { + l.on_debug(&format!( + "Template source: {}/grafana/", + environment.templates_dir().display() + )); + } + let clock = Arc::new(SystemClock); let step = RenderGrafanaTemplatesStep::new( Arc::new(environment.clone()), @@ -130,6 +170,10 @@ fn render_templates( ) })?; + if let Some(l) = listener { + l.on_detail("Rendering Grafana provisioning files (datasources, dashboards)"); + } + info!( command = "release", step = %current_step, @@ -141,15 +185,25 @@ fn render_templates( /// Deploy Grafana provisioning configuration to the remote host /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, `ReleaseStep::DeployGrafanaProvisioning`) if deployment fails #[allow(clippy::result_large_err)] fn deploy_provisioning_to_remote( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { let current_step = ReleaseStep::DeployGrafanaProvisioning; + if let Some(l) = listener { + l.on_debug("Executing playbook: ansible-playbook deploy-grafana-provisioning.yml"); + } + DeployGrafanaProvisioningStep::new(ansible_client(environment)) .execute() .map_err(|e| { @@ -162,6 +216,10 @@ fn deploy_provisioning_to_remote( ) })?; + if let Some(l) = listener { + l.on_detail("Deploying provisioning to /opt/torrust/storage/grafana/provisioning"); + } + info!( command = "release", step = %current_step, diff --git a/src/application/command_handlers/release/steps/mysql.rs b/src/application/command_handlers/release/steps/mysql.rs index bfdfad5a..19eecb57 100644 --- a/src/application/command_handlers/release/steps/mysql.rs +++ b/src/application/command_handlers/release/steps/mysql.rs @@ -11,6 +11,7 @@ use super::common::ansible_client; use crate::application::command_handlers::common::StepResult; use crate::application::command_handlers::release::errors::ReleaseCommandHandlerError; use crate::application::steps::application::CreateMysqlStorageStep; +use crate::application::traits::CommandProgressListener; use crate::domain::environment::state::ReleaseStep; use crate::domain::environment::{Environment, Releasing}; @@ -21,12 +22,18 @@ use crate::domain::environment::{Environment, Releasing}; /// /// If `MySQL` is not configured as the tracker database, all steps are skipped. /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, step) if `MySQL` storage creation fails #[allow(clippy::result_large_err)] pub fn release( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { // Check if MySQL is configured (via tracker database driver) if !environment.context().user_inputs.tracker().uses_mysql() { @@ -39,21 +46,35 @@ pub fn release( return Ok(()); } - create_storage(environment)?; + create_storage(environment, listener)?; Ok(()) } /// Create `MySQL` storage directories on the remote host /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, `ReleaseStep::CreateMysqlStorage`) if creation fails #[allow(clippy::result_large_err)] fn create_storage( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { let current_step = ReleaseStep::CreateMysqlStorage; + if let Some(l) = listener { + l.on_debug(&format!( + "Ansible working directory: {}", + environment.ansible_build_dir().display() + )); + l.on_debug("Executing playbook: ansible-playbook create-mysql-storage.yml"); + } + CreateMysqlStorageStep::new(ansible_client(environment)) .execute() .map_err(|e| { @@ -66,6 +87,10 @@ fn create_storage( ) })?; + if let Some(l) = listener { + l.on_detail("Creating storage directories: /opt/torrust/storage/mysql/data"); + } + info!( command = "release", step = %current_step, diff --git a/src/application/command_handlers/release/steps/prometheus.rs b/src/application/command_handlers/release/steps/prometheus.rs index 5294bcbe..ca1028a8 100644 --- a/src/application/command_handlers/release/steps/prometheus.rs +++ b/src/application/command_handlers/release/steps/prometheus.rs @@ -18,6 +18,7 @@ use crate::application::steps::application::{ CreatePrometheusStorageStep, DeployPrometheusConfigStep, }; use crate::application::steps::rendering::RenderPrometheusTemplatesStep; +use crate::application::traits::CommandProgressListener; use crate::domain::environment::state::ReleaseStep; use crate::domain::environment::{Environment, Releasing}; use crate::shared::clock::SystemClock; @@ -31,12 +32,18 @@ use crate::shared::clock::SystemClock; /// /// If Prometheus is not configured, all steps are skipped. /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, step) if any Prometheus step fails #[allow(clippy::result_large_err)] pub fn release( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { // Check if Prometheus is configured if environment.context().user_inputs.prometheus().is_none() { @@ -49,23 +56,37 @@ pub fn release( return Ok(()); } - create_storage(environment)?; - render_templates(environment)?; - deploy_config_to_remote(environment)?; + create_storage(environment, listener)?; + render_templates(environment, listener)?; + deploy_config_to_remote(environment, listener)?; Ok(()) } /// Create Prometheus storage directories on the remote host /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, `ReleaseStep::CreatePrometheusStorage`) if creation fails #[allow(clippy::result_large_err)] fn create_storage( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { let current_step = ReleaseStep::CreatePrometheusStorage; + if let Some(l) = listener { + l.on_debug(&format!( + "Ansible working directory: {}", + environment.ansible_build_dir().display() + )); + l.on_debug("Executing playbook: ansible-playbook create-prometheus-storage.yml"); + } + CreatePrometheusStorageStep::new(ansible_client(environment)) .execute() .map_err(|e| { @@ -78,6 +99,10 @@ fn create_storage( ) })?; + if let Some(l) = listener { + l.on_detail("Creating storage directories: /opt/torrust/storage/prometheus/etc"); + } + info!( command = "release", step = %current_step, @@ -89,15 +114,28 @@ fn create_storage( /// Render Prometheus configuration templates to the build directory /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, `ReleaseStep::RenderPrometheusTemplates`) if rendering fails #[allow(clippy::result_large_err)] fn render_templates( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { let current_step = ReleaseStep::RenderPrometheusTemplates; + if let Some(l) = listener { + l.on_debug(&format!( + "Template source: {}/prometheus/", + environment.templates_dir().display() + )); + } + let clock = Arc::new(SystemClock); let step = RenderPrometheusTemplatesStep::new( Arc::new(environment.clone()), @@ -116,6 +154,10 @@ fn render_templates( ) })?; + if let Some(l) = listener { + l.on_detail("Rendering prometheus.yml from template"); + } + info!( command = "release", step = %current_step, @@ -127,15 +169,25 @@ fn render_templates( /// Deploy Prometheus configuration to the remote host via Ansible /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, `ReleaseStep::DeployPrometheusConfigToRemote`) if deployment fails #[allow(clippy::result_large_err)] fn deploy_config_to_remote( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { let current_step = ReleaseStep::DeployPrometheusConfigToRemote; + if let Some(l) = listener { + l.on_debug("Executing playbook: ansible-playbook deploy-prometheus-config.yml"); + } + DeployPrometheusConfigStep::new(ansible_client(environment)) .execute() .map_err(|e| { @@ -148,6 +200,10 @@ fn deploy_config_to_remote( ) })?; + if let Some(l) = listener { + l.on_detail("Deploying config to /opt/torrust/storage/prometheus/etc/prometheus.yml"); + } + info!( command = "release", step = %current_step, diff --git a/src/application/command_handlers/release/steps/tracker.rs b/src/application/command_handlers/release/steps/tracker.rs index 7a26f531..642fafd5 100644 --- a/src/application/command_handlers/release/steps/tracker.rs +++ b/src/application/command_handlers/release/steps/tracker.rs @@ -18,6 +18,7 @@ use crate::application::steps::application::{ CreateTrackerStorageStep, DeployTrackerConfigStep, InitTrackerDatabaseStep, }; use crate::application::steps::rendering::RenderTrackerTemplatesStep; +use crate::application::traits::CommandProgressListener; use crate::domain::environment::state::ReleaseStep; use crate::domain::environment::{Environment, Releasing}; use crate::shared::SystemClock; @@ -30,31 +31,51 @@ use crate::shared::SystemClock; /// 3. Render configuration templates /// 4. Deploy configuration to remote /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, step) if any tracker step fails #[allow(clippy::result_large_err)] pub fn release( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { - create_storage(environment)?; - init_database(environment)?; - let tracker_build_dir = render_templates(environment)?; - deploy_config_to_remote(environment, &tracker_build_dir)?; + create_storage(environment, listener)?; + init_database(environment, listener)?; + let tracker_build_dir = render_templates(environment, listener)?; + deploy_config_to_remote(environment, &tracker_build_dir, listener)?; Ok(()) } /// Create tracker storage directories on the remote host /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, `ReleaseStep::CreateTrackerStorage`) if creation fails #[allow(clippy::result_large_err)] fn create_storage( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { let current_step = ReleaseStep::CreateTrackerStorage; + if let Some(l) = listener { + l.on_debug(&format!( + "Ansible working directory: {}", + environment.ansible_build_dir().display() + )); + l.on_debug("Executing playbook: ansible-playbook create-tracker-storage.yml"); + } + CreateTrackerStorageStep::new(ansible_client(environment)) .execute() .map_err(|e| { @@ -67,6 +88,10 @@ fn create_storage( ) })?; + if let Some(l) = listener { + l.on_detail("Creating storage directories: /opt/torrust/storage/tracker/{lib,log,etc}"); + } + info!( command = "release", step = %current_step, @@ -78,15 +103,25 @@ fn create_storage( /// Initialize tracker database on the remote host /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, `ReleaseStep::InitTrackerDatabase`) if initialization fails #[allow(clippy::result_large_err)] fn init_database( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { let current_step = ReleaseStep::InitTrackerDatabase; + if let Some(l) = listener { + l.on_debug("Executing playbook: ansible-playbook init-tracker-database.yml"); + } + InitTrackerDatabaseStep::new(ansible_client(environment)) .execute() .map_err(|e| { @@ -99,6 +134,10 @@ fn init_database( ) })?; + if let Some(l) = listener { + l.on_detail("Initializing database: tracker.db"); + } + info!( command = "release", step = %current_step, @@ -110,15 +149,28 @@ fn init_database( /// Render Tracker configuration templates to the build directory /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for detail and debug reporting +/// /// # Errors /// /// Returns a tuple of (error, `ReleaseStep::RenderTrackerTemplates`) if rendering fails #[allow(clippy::result_large_err)] fn render_templates( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult { let current_step = ReleaseStep::RenderTrackerTemplates; + if let Some(l) = listener { + l.on_debug(&format!( + "Template source: {}/tracker/", + environment.templates_dir().display() + )); + } + let clock = Arc::new(SystemClock); let step = RenderTrackerTemplatesStep::new( Arc::new(environment.clone()), @@ -137,6 +189,11 @@ fn render_templates( ) })?; + if let Some(l) = listener { + l.on_detail("Rendering tracker.toml from template"); + l.on_debug(&format!("Template output: {}", tracker_build_dir.display())); + } + info!( command = "release", tracker_build_dir = %tracker_build_dir.display(), @@ -152,6 +209,7 @@ fn render_templates( /// /// * `environment` - The environment in Releasing state /// * `tracker_build_dir` - Path to the rendered tracker configuration +/// * `listener` - Optional progress listener for detail and debug reporting /// /// # Errors /// @@ -160,9 +218,14 @@ fn render_templates( fn deploy_config_to_remote( environment: &Environment, tracker_build_dir: &Path, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult<(), ReleaseCommandHandlerError, ReleaseStep> { let current_step = ReleaseStep::DeployTrackerConfigToRemote; + if let Some(l) = listener { + l.on_debug("Executing playbook: ansible-playbook deploy-tracker-config.yml"); + } + DeployTrackerConfigStep::new(ansible_client(environment), tracker_build_dir.to_path_buf()) .execute() .map_err(|e| { @@ -175,6 +238,10 @@ fn deploy_config_to_remote( ) })?; + if let Some(l) = listener { + l.on_detail("Deploying config to /opt/torrust/storage/tracker/etc/tracker.toml"); + } + info!( command = "release", step = %current_step, diff --git a/src/application/command_handlers/release/tests.rs b/src/application/command_handlers/release/tests.rs index 30a5b8b2..16036889 100644 --- a/src/application/command_handlers/release/tests.rs +++ b/src/application/command_handlers/release/tests.rs @@ -34,7 +34,7 @@ async fn it_should_return_environment_not_found_error_when_environment_does_not_ let (handler, _temp_dir) = create_test_handler(); let env_name = EnvironmentName::new("nonexistent-env").unwrap(); - let result = handler.execute(&env_name).await; + let result = handler.execute(&env_name, None).await; assert!(result.is_err()); let error = result.unwrap_err(); diff --git a/src/application/command_handlers/release/workflow.rs b/src/application/command_handlers/release/workflow.rs index f83b4e49..ab115bb3 100644 --- a/src/application/command_handlers/release/workflow.rs +++ b/src/application/command_handlers/release/workflow.rs @@ -4,8 +4,10 @@ //! all service-specific release steps in the correct order. use super::errors::ReleaseCommandHandlerError; +use super::handler::TOTAL_RELEASE_STEPS; use super::steps::{backup, caddy, compose, grafana, mysql, prometheus, tracker}; use crate::application::command_handlers::common::StepResult; +use crate::application::traits::CommandProgressListener; use crate::domain::environment::state::ReleaseStep; use crate::domain::environment::{Environment, Released, Releasing}; @@ -15,19 +17,59 @@ use crate::domain::environment::{Environment, Released, Releasing}; /// release steps in the correct order. Each service module handles its /// own conditional logic (e.g., skipping if not enabled). /// +/// # Arguments +/// +/// * `environment` - The environment in Releasing state +/// * `listener` - Optional progress listener for step-level reporting +/// /// # Errors /// /// Returns a tuple of (error, `current_step`) if any release step fails pub async fn execute( environment: &Environment, + listener: Option<&dyn CommandProgressListener>, ) -> StepResult, ReleaseCommandHandlerError, ReleaseStep> { - tracker::release(environment)?; - prometheus::release(environment)?; - grafana::release(environment)?; - mysql::release(environment)?; - backup::release(environment).await?; - caddy::release(environment)?; - compose::release(environment).await?; + // Step 1/7: Release Tracker service + notify_step_started(listener, 1, "Releasing Tracker service"); + tracker::release(environment, listener)?; + + // Step 2/7: Release Prometheus service + notify_step_started(listener, 2, "Releasing Prometheus service"); + prometheus::release(environment, listener)?; + + // Step 3/7: Release Grafana service + notify_step_started(listener, 3, "Releasing Grafana service"); + grafana::release(environment, listener)?; + + // Step 4/7: Release MySQL service + notify_step_started(listener, 4, "Releasing MySQL service"); + mysql::release(environment, listener)?; + + // Step 5/7: Release Backup service + notify_step_started(listener, 5, "Releasing Backup service"); + backup::release(environment, listener).await?; + + // Step 6/7: Release Caddy service + notify_step_started(listener, 6, "Releasing Caddy service"); + caddy::release(environment, listener)?; + + // Step 7/7: Deploy Docker Compose configuration + notify_step_started(listener, 7, "Deploying Docker Compose configuration"); + compose::release(environment, listener).await?; Ok(environment.clone().released()) } + +/// Notify the progress listener that a step has started. +/// +/// This is a convenience helper that handles the `Option` check, +/// keeping the step-reporting code in the workflow clean. +fn notify_step_started( + listener: Option<&dyn CommandProgressListener>, + step_number: usize, + description: &str, +) { + if let Some(l) = listener { + l.on_step_started(step_number, TOTAL_RELEASE_STEPS, description); + } +} diff --git a/src/presentation/controllers/release/handler.rs b/src/presentation/controllers/release/handler.rs index f5dc6ae9..7fd71a20 100644 --- a/src/presentation/controllers/release/handler.rs +++ b/src/presentation/controllers/release/handler.rs @@ -12,7 +12,7 @@ use tracing::info; use crate::application::command_handlers::release::ReleaseCommandHandler; use crate::domain::environment::name::EnvironmentName; use crate::domain::environment::repository::EnvironmentRepository; -use crate::presentation::views::progress::ProgressReporter; +use crate::presentation::views::progress::{ProgressReporter, VerboseProgressListener}; use crate::presentation::views::UserOutput; use crate::shared::clock::Clock; @@ -157,8 +157,13 @@ impl ReleaseCommandController { let handler = ReleaseCommandHandler::new(self.repository.clone(), self.clock.clone()); + // Create the listener for verbose progress reporting. + // The VerboseProgressListener translates step events into + // user-facing detail messages via UserOutput's verbosity filter. + let listener = VerboseProgressListener::new(self.progress.output().clone()); + let _released_env = handler - .execute(env_name) + .execute(env_name, Some(&listener)) .await .map_err(|source| ReleaseSubcommandError::ApplicationLayerError { source })?;