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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/user-guide/commands/render.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,34 @@ The render command generates artifacts in the specified output directory:
- **Docker Compose** - Complete service stack definition
- **Configuration files** - All service configurations rendered

### JSON Output

Use `--output-format json` (or `-o json`) to get machine-readable output. Progress messages go to stderr; the JSON result goes to stdout.

```bash
torrust-tracker-deployer render \
--env-file envs/my-environment.json \
--instance-ip 192.168.1.100 \
--output-dir /tmp/build/my-environment \
--output-format json 2>/dev/null
```

```json
{
"environment_name": "my-environment",
"config_source": "Config file: envs/my-environment.json",
"target_ip": "192.168.1.100",
"output_dir": "/tmp/build/my-environment"
}
```

| Field | Type | Description |
| ------------------ | ------ | ----------------------------------------------------------------- |
| `environment_name` | string | Name of the environment whose artifacts were generated |
| `config_source` | string | Description of the configuration source (env name or config file) |
| `target_ip` | string | IP address used in artifact generation |
| `output_dir` | string | Path to the directory containing generated artifacts |

## Use Cases

### 1. Preview Before Provisioning
Expand Down
71 changes: 36 additions & 35 deletions src/presentation/cli/controllers/render/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ use std::sync::Arc;

use parking_lot::ReentrantMutex;

use crate::application::command_handlers::render::{RenderCommandHandler, RenderInputMode};
use crate::application::command_handlers::render::{
RenderCommandHandler, RenderInputMode, RenderResult,
};
use crate::domain::environment::repository::EnvironmentRepository;
use crate::domain::EnvironmentName;
use crate::presentation::cli::input::cli::OutputFormat;
use crate::presentation::cli::views::commands::render::{JsonView, RenderDetailsData, TextView};
use crate::presentation::cli::views::progress::ProgressReporter;
use crate::presentation::cli::views::UserOutput;

Expand Down Expand Up @@ -70,7 +74,6 @@ impl RenderStep {
pub struct RenderCommandController {
handler: RenderCommandHandler,
progress: ProgressReporter,
user_output: Arc<ReentrantMutex<RefCell<UserOutput>>>,
}

impl RenderCommandController {
Expand All @@ -85,8 +88,7 @@ impl RenderCommandController {
) -> Self {
Self {
handler: RenderCommandHandler::new(repository),
progress: ProgressReporter::new(Arc::clone(&user_output), RenderStep::count()),
user_output,
progress: ProgressReporter::new(user_output, RenderStep::count()),
}
}

Expand All @@ -102,6 +104,7 @@ impl RenderCommandController {
/// * `output_dir` - Output directory for generated artifacts (required)
/// * `force` - Whether to overwrite existing output directory
/// * `working_dir` - Working directory for environment data (from --working-dir global arg)
/// * `output_format` - Output format (text or JSON)
///
/// # Returns
///
Expand All @@ -117,6 +120,7 @@ impl RenderCommandController {
/// - Config file doesn't exist
/// - Environment not found
/// - Template rendering fails
#[allow(clippy::too_many_arguments)] // Required parameters for render workflow - all are necessary
pub async fn execute(
&mut self,
env_name: Option<&str>,
Expand All @@ -125,6 +129,7 @@ impl RenderCommandController {
output_dir: &Path,
force: bool,
working_dir: &Path,
output_format: OutputFormat,
) -> Result<(), RenderCommandError> {
// Step 1: Validate input
self.progress
Expand All @@ -136,18 +141,15 @@ impl RenderCommandController {
.map_err(|_| RenderCommandError::InvalidIpAddress { ip: ip.to_string() })?;

// Determine input mode and prepare handler parameters
let (input_mode, source_desc) = match (env_name, env_file) {
let input_mode = match (env_name, env_file) {
(Some(name), None) => {
let env_name = EnvironmentName::new(name).map_err(|e| {
RenderCommandError::InvalidEnvironmentName {
value: name.to_string(),
reason: e.to_string(),
}
})?;
(
RenderInputMode::EnvironmentName(env_name.clone()),
format!("Environment: {env_name}"),
)
RenderInputMode::EnvironmentName(env_name)
}
(None, Some(path)) => {
// Validate file exists
Expand All @@ -156,10 +158,7 @@ impl RenderCommandController {
path: path.to_path_buf(),
});
}
(
RenderInputMode::ConfigFile(path.to_path_buf()),
format!("Config file: {}", path.display()),
)
RenderInputMode::ConfigFile(path.to_path_buf())
}
(None, None) => return Err(RenderCommandError::NoInputMode),
(Some(_), Some(_)) => unreachable!("Clap ensures mutual exclusivity"),
Expand Down Expand Up @@ -189,31 +188,33 @@ impl RenderCommandController {

self.progress.complete_step(None)?;

// Show success message
self.show_success(
&source_desc,
&result.target_ip.to_string(),
&result.output_dir,
);
// Render and display results
self.complete_workflow(&result, output_format)?;

Ok(())
}

/// Show success message to user
fn show_success(&mut self, source: &str, target_ip: &str, output_dir: &Path) {
let output = self.user_output.lock();
let mut output_ref = output.borrow_mut();

output_ref.success(&format!(
"\nDeployment artifacts generated successfully!\n\n \
Source: {source}\n \
Target IP: {target_ip}\n \
Output: {}\n\n\
Next steps:\n \
- Review artifacts in the output directory\n \
- Use 'provision' command to deploy infrastructure\n \
- Or use artifacts manually with your deployment tools",
output_dir.display()
));
/// Complete the workflow with render details output
///
/// Renders the artifact generation summary using the chosen output format
/// (text or JSON) and displays it to the user.
fn complete_workflow(
&mut self,
result: &RenderResult,
output_format: OutputFormat,
) -> Result<(), RenderCommandError> {
let data = RenderDetailsData::from_result(result);

match output_format {
OutputFormat::Text => {
self.progress.blank_line()?;
self.progress.complete(&TextView::render(&data))?;
}
OutputFormat::Json => {
self.progress.result(&JsonView::render(&data))?;
}
}

Ok(())
}
}
2 changes: 2 additions & 0 deletions src/presentation/cli/dispatch/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ pub async fn route_command(
output_dir,
force,
} => {
let output_format = context.output_format();
context
.container()
.create_render_controller()
Expand All @@ -203,6 +204,7 @@ pub async fn route_command(
output_dir.as_path(),
force,
context.working_dir(),
output_format,
)
.await?;
Ok(())
Expand Down
1 change: 1 addition & 0 deletions src/presentation/cli/views/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod destroy;
pub mod list;
pub mod provision;
pub mod release;
pub mod render;
pub mod run;
pub mod shared;
pub mod show;
Expand Down
52 changes: 52 additions & 0 deletions src/presentation/cli/views/commands/render/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//! Views for Render Command
//!
//! This module contains view components for rendering render command output.
//!
//! # Architecture
//!
//! This module follows the Strategy Pattern for rendering:
//! - `RenderDetailsData`: The data DTO passed to all views
//! - `TextView`: Renders human-readable text output
//! - `JsonView`: Renders machine-readable JSON output
//!
//! # Structure
//!
//! - `view_data/`: Data structures (DTOs) passed to views
//! - `render_details.rs`: Main DTO with render result data
//! - `views/`: View rendering implementations
//! - `text_view.rs`: Human-readable text rendering
//! - `json_view.rs`: Machine-readable JSON rendering
//!
//! # SOLID Principles
//!
//! - **Single Responsibility**: Each view has one job - render in its format
//! - **Open/Closed**: Add new formats by creating new view files, not modifying existing ones
//! - **Strategy Pattern**: Different rendering strategies for the same data
//!
//! # Adding New Formats
//!
//! To add a new output format (e.g., XML, YAML, CSV):
//! 1. Create a new file in `views/`: `xml_view.rs`, `yaml_view.rs`, etc.
//! 2. Implement the view with `render(data: &RenderDetailsData) -> String`
//! 3. Export it from this module
//! 4. No need to modify existing views or the DTO

pub mod view_data {
pub mod render_details;

// Re-export main types for convenience
pub use render_details::RenderDetailsData;
}

pub mod views {
pub mod json_view;
pub mod text_view;

// Re-export views for convenience
pub use json_view::JsonView;
pub use text_view::TextView;
}

// Re-export at module root for convenience
pub use view_data::RenderDetailsData;
pub use views::{JsonView, TextView};
Loading
Loading