Skip to content

Commit 84994a1

Browse files
committed
docs: [#41] update issue spec to reflect PR #48 implementation
- Create v2 spec with correct architecture (domain layer, not infrastructure) - Update API to use EnvironmentCreationConfig::generate_template_file() - Simplify implementation guide (CLI wrapper only) - Deprecate v1 spec with clear migration path - Remove obsolete infrastructure/templates references - Add comprehensive testing guidance - Clarify this is thin CLI integration, not new logic
1 parent 66e49c5 commit 84994a1

File tree

2 files changed

+406
-6
lines changed

2 files changed

+406
-6
lines changed
Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
# Template Generation Support (CLI Integration)
2+
3+
**Issue**: [#41](https://github.com/torrust/torrust-tracker-deployer/issues/41)
4+
**Parent Epic**: [#34](https://github.com/torrust/torrust-tracker-deployer/issues/34) - Implement Create Environment Command
5+
**Depends On**: [#40](https://github.com/torrust/torrust-tracker-deployer/issues/40) - Template System Integration ✅ **COMPLETED in PR #48**
6+
**Status**: READY TO IMPLEMENT
7+
**Related**: [Roadmap Task 1.5](../roadmap.md)
8+
9+
## Overview
10+
11+
Implement the `template` subcommand for the create command, allowing users to generate configuration file templates via the CLI. This is a **thin CLI layer** that calls the existing template generation functionality implemented in PR #48.
12+
13+
**Key Point**: The template generation logic is **already complete** in `src/domain/config/environment_config.rs` (PR #48). This issue is only about adding the CLI subcommand to expose that functionality to users.
14+
15+
## Goals
16+
17+
- [ ] Add `template` subcommand to the create command CLI structure
18+
- [ ] Call existing `EnvironmentCreationConfig::generate_template_file()` method
19+
- [ ] Support custom output paths for template generation
20+
- [ ] Provide helpful success messages with next steps
21+
22+
**Estimated Time**: 1-2 hours (simple CLI integration)
23+
24+
## 🏗️ Architecture Requirements
25+
26+
**DDD Layer**: Presentation Layer only (thin CLI wrapper)
27+
**Module Path**: `src/presentation/console/subcommands/create/`
28+
**Pattern**: CLI Subcommand → Domain Method Call
29+
30+
**What's Already Done** ✅:
31+
32+
- Template generation logic: `EnvironmentCreationConfig::generate_template_file()` in `src/domain/config/environment_config.rs`
33+
- Error handling: `CreateConfigError` with template-specific variants
34+
- Tests: Comprehensive test coverage in domain layer
35+
36+
**What Needs to Be Done**:
37+
38+
- CLI subcommand argument parsing
39+
- Handler to call the existing domain method
40+
- User-friendly success messages
41+
42+
## CLI Interface Specification
43+
44+
```bash
45+
# Generate template configuration file (JSON format)
46+
torrust-tracker-deployer create template
47+
# Creates: ./environment-template.json in current working directory
48+
49+
# Generate template in specific directory
50+
torrust-tracker-deployer create template ./config/environment.json
51+
52+
# Generate template with custom filename
53+
torrust-tracker-deployer create template ./my-environment.json
54+
55+
# Show help for template subcommand
56+
torrust-tracker-deployer create template --help
57+
```
58+
59+
## Implementation Guide
60+
61+
### Step 1: Extend CLI Arguments
62+
63+
Update `src/presentation/console/subcommands/create/args.rs`:
64+
65+
```rust
66+
use clap::{Args, Subcommand};
67+
use std::path::PathBuf;
68+
69+
/// Arguments for the create subcommand
70+
#[derive(Debug, Args)]
71+
pub struct CreateArgs {
72+
#[command(subcommand)]
73+
pub action: CreateAction,
74+
}
75+
76+
/// Actions available for the create command
77+
#[derive(Debug, Subcommand)]
78+
pub enum CreateAction {
79+
/// Create environment from configuration file
80+
Environment {
81+
/// Path to the environment configuration file
82+
#[arg(long, short = 'f', value_name = "FILE")]
83+
env_file: PathBuf,
84+
},
85+
86+
/// Generate template configuration file
87+
Template {
88+
/// Output path for the template file (optional)
89+
/// If not provided, creates environment-template.json in current directory
90+
#[arg(value_name = "PATH")]
91+
output_path: Option<PathBuf>,
92+
},
93+
}
94+
95+
impl CreateAction {
96+
/// Get the default template output path
97+
pub fn default_template_path() -> PathBuf {
98+
PathBuf::from("environment-template.json")
99+
}
100+
}
101+
```
102+
103+
### Step 2: Implement Handler
104+
105+
Update `src/presentation/console/subcommands/create/handler.rs`:
106+
107+
```rust
108+
use super::{CreateArgs, CreateAction};
109+
use crate::domain::config::EnvironmentCreationConfig;
110+
use std::path::PathBuf;
111+
112+
pub async fn handle_create_command(args: CreateArgs) -> Result<(), Box<dyn std::error::Error>> {
113+
match args.action {
114+
CreateAction::Template { output_path } => {
115+
let template_path = output_path
116+
.unwrap_or_else(|| CreateAction::default_template_path());
117+
handle_template_generation(template_path).await
118+
}
119+
CreateAction::Environment { env_file } => {
120+
handle_environment_creation(env_file).await
121+
}
122+
}
123+
}
124+
125+
async fn handle_template_generation(output_path: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
126+
println!("Generating configuration template...");
127+
128+
// Call existing domain method - template generation implemented in PR #48
129+
EnvironmentCreationConfig::generate_template_file(&output_path).await?;
130+
131+
println!("✅ Configuration template generated: {}", output_path.display());
132+
println!();
133+
println!("Next steps:");
134+
println!("1. Edit the template file and replace placeholder values:");
135+
println!(" - REPLACE_WITH_ENVIRONMENT_NAME: Choose a unique environment name (e.g., 'dev', 'staging')");
136+
println!(" - REPLACE_WITH_SSH_PRIVATE_KEY_PATH: Path to your SSH private key");
137+
println!(" - REPLACE_WITH_SSH_PUBLIC_KEY_PATH: Path to your SSH public key");
138+
println!("2. Review default values:");
139+
println!(" - username: 'torrust' (can be changed if needed)");
140+
println!(" - port: 22 (standard SSH port)");
141+
println!("3. Create the environment:");
142+
println!(" torrust-tracker-deployer create environment --env-file {}", output_path.display());
143+
144+
Ok(())
145+
}
146+
147+
async fn handle_environment_creation(config_path: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
148+
// Existing environment creation logic from other subissues
149+
todo!("Environment creation logic - implemented in previous subissues")
150+
}
151+
```
152+
153+
### Step 3: Update Module Exports
154+
155+
Ensure `src/presentation/console/subcommands/create/mod.rs` exports the necessary types:
156+
157+
```rust
158+
mod args;
159+
mod handler;
160+
161+
pub use args::{CreateArgs, CreateAction};
162+
pub use handler::handle_create_command;
163+
```
164+
165+
## Generated Template Format
166+
167+
The template generated by `EnvironmentCreationConfig::generate_template_file()` looks like this:
168+
169+
```json
170+
{
171+
"environment": {
172+
"name": "REPLACE_WITH_ENVIRONMENT_NAME"
173+
},
174+
"ssh_credentials": {
175+
"private_key_path": "REPLACE_WITH_SSH_PRIVATE_KEY_PATH",
176+
"public_key_path": "REPLACE_WITH_SSH_PUBLIC_KEY_PATH",
177+
"username": "torrust",
178+
"port": 22
179+
}
180+
}
181+
```
182+
183+
**Note**: This format is guaranteed to be valid and up-to-date because it's generated directly from the `EnvironmentCreationConfig` struct using serde serialization.
184+
185+
## Error Handling
186+
187+
Errors are automatically handled by the existing `CreateConfigError` enum (implemented in PR #48):
188+
189+
- `TemplateSerializationFailed`: JSON serialization errors (should never happen)
190+
- `TemplateDirectoryCreationFailed`: Parent directory cannot be created
191+
- `TemplateFileWriteFailed`: File cannot be written
192+
193+
All errors include `.help()` methods with actionable troubleshooting guidance. The CLI handler just needs to propagate these errors using `?` operator.
194+
195+
## Testing
196+
197+
### Unit Tests for CLI Arguments
198+
199+
Add to `src/presentation/console/subcommands/create/args.rs`:
200+
201+
```rust
202+
#[cfg(test)]
203+
mod tests {
204+
use super::*;
205+
206+
#[test]
207+
fn it_should_use_default_template_path_when_none_provided() {
208+
let default_path = CreateAction::default_template_path();
209+
assert_eq!(default_path, PathBuf::from("environment-template.json"));
210+
}
211+
212+
#[test]
213+
fn it_should_parse_template_action_with_custom_path() {
214+
// Test with clap parsing if needed
215+
// This tests the CLI argument structure
216+
}
217+
}
218+
```
219+
220+
### Integration Test for Handler
221+
222+
Add to `src/presentation/console/subcommands/create/handler.rs`:
223+
224+
```rust
225+
#[cfg(test)]
226+
mod tests {
227+
use super::*;
228+
use tempfile::TempDir;
229+
230+
#[tokio::test]
231+
async fn it_should_generate_template_with_default_path() {
232+
let temp_dir = TempDir::new().unwrap();
233+
std::env::set_current_dir(temp_dir.path()).unwrap();
234+
235+
let args = CreateArgs {
236+
action: CreateAction::Template {
237+
output_path: None,
238+
},
239+
};
240+
241+
let result = handle_create_command(args).await;
242+
assert!(result.is_ok());
243+
244+
// Verify file exists
245+
let template_path = temp_dir.path().join("environment-template.json");
246+
assert!(template_path.exists());
247+
}
248+
249+
#[tokio::test]
250+
async fn it_should_generate_template_with_custom_path() {
251+
let temp_dir = TempDir::new().unwrap();
252+
let custom_path = temp_dir.path().join("config").join("my-env.json");
253+
254+
let args = CreateArgs {
255+
action: CreateAction::Template {
256+
output_path: Some(custom_path.clone()),
257+
},
258+
};
259+
260+
let result = handle_create_command(args).await;
261+
assert!(result.is_ok());
262+
263+
// Verify file exists at custom path
264+
assert!(custom_path.exists());
265+
// Verify parent directory was created
266+
assert!(custom_path.parent().unwrap().exists());
267+
}
268+
269+
#[tokio::test]
270+
async fn it_should_generate_valid_json_template() {
271+
let temp_dir = TempDir::new().unwrap();
272+
let template_path = temp_dir.path().join("test.json");
273+
274+
let args = CreateArgs {
275+
action: CreateAction::Template {
276+
output_path: Some(template_path.clone()),
277+
},
278+
};
279+
280+
handle_create_command(args).await.unwrap();
281+
282+
// Read and parse the generated template
283+
let content = std::fs::read_to_string(&template_path).unwrap();
284+
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
285+
286+
// Verify structure
287+
assert!(parsed["environment"]["name"].is_string());
288+
assert!(parsed["ssh_credentials"]["private_key_path"].is_string());
289+
assert_eq!(parsed["ssh_credentials"]["username"], "torrust");
290+
assert_eq!(parsed["ssh_credentials"]["port"], 22);
291+
}
292+
}
293+
```
294+
295+
**Note**: The domain-layer tests in `src/domain/config/environment_config.rs` already cover the template generation logic comprehensively. These CLI tests only verify the CLI integration works correctly.
296+
297+
## Acceptance Criteria
298+
299+
- [ ] `torrust-tracker-deployer create template` generates `environment-template.json` in current directory
300+
- [ ] `torrust-tracker-deployer create template ./config/env.json` generates template at specified path
301+
- [ ] Generated template contains valid JSON structure with placeholder values
302+
- [ ] Template generation creates parent directories if they don't exist
303+
- [ ] Clear success message with next steps instructions after template generation
304+
- [ ] Proper error handling with actionable help messages (inherited from domain layer)
305+
- [ ] Template subcommand integrates cleanly with existing create command structure
306+
- [ ] Help system works correctly: `torrust-tracker-deployer create template --help`
307+
- [ ] Unit tests verify CLI argument parsing
308+
- [ ] Integration tests verify end-to-end template generation via CLI
309+
310+
## Success Criteria
311+
312+
When complete, users should be able to:
313+
314+
1. **Generate a template**: `torrust-tracker-deployer create template`
315+
2. **See clear output**:
316+
317+
```text
318+
Generating configuration template...
319+
✅ Configuration template generated: environment-template.json
320+
321+
Next steps:
322+
1. Edit the template file and replace placeholder values:
323+
- REPLACE_WITH_ENVIRONMENT_NAME: Choose a unique environment name (e.g., 'dev', 'staging')
324+
- REPLACE_WITH_SSH_PRIVATE_KEY_PATH: Path to your SSH private key
325+
- REPLACE_WITH_SSH_PUBLIC_KEY_PATH: Path to your SSH public key
326+
2. Review default values:
327+
- username: 'torrust' (can be changed if needed)
328+
- port: 22 (standard SSH port)
329+
3. Create the environment:
330+
torrust-tracker-deployer create environment --env-file environment-template.json
331+
```
332+
333+
3. **Edit the template** with their actual values
334+
4. **Create the environment** using the edited configuration
335+
336+
## Implementation Notes
337+
338+
### What NOT to Do
339+
340+
**Don't create new template generation logic** - it's already done in the domain layer
341+
**Don't create infrastructure code** - `src/infrastructure/templates/` is not needed
342+
**Don't duplicate error types** - use existing `CreateConfigError` from domain layer
343+
**Don't implement synchronous file operations** - the domain method is already async
344+
345+
### What TO Do
346+
347+
**Call existing domain method**: `EnvironmentCreationConfig::generate_template_file(&path).await`
348+
**Keep it simple**: This is just a thin CLI wrapper
349+
**Focus on user experience**: Good error messages and clear next steps
350+
**Write integration tests**: Test the CLI flow, not the template generation (already tested)
351+
352+
### Key API Reference
353+
354+
```rust
355+
// Domain layer - already implemented in PR #48
356+
impl EnvironmentCreationConfig {
357+
/// Creates a template instance with placeholder values
358+
pub fn template() -> Self;
359+
360+
/// Generates a configuration template file at the specified path
361+
/// - Creates parent directories if needed
362+
/// - Generates pretty-printed JSON
363+
/// - Returns CreateConfigError on failure
364+
pub async fn generate_template_file(path: &std::path::Path) -> Result<(), CreateConfigError>;
365+
}
366+
367+
// Error type - already implemented in PR #48
368+
pub enum CreateConfigError {
369+
TemplateSerializationFailed { source: serde_json::Error },
370+
TemplateDirectoryCreationFailed { path: PathBuf, source: std::io::Error },
371+
TemplateFileWriteFailed { path: PathBuf, source: std::io::Error },
372+
// ... other variants
373+
}
374+
```
375+
376+
## Future Enhancements
377+
378+
This implementation provides the foundation for future template features:
379+
380+
- **TOML template support**: Add `--format` flag to choose JSON or TOML
381+
- **Interactive template generation**: Guided template creation with prompts
382+
- **Template examples**: Pre-filled templates for common scenarios
383+
- **Template validation**: Pre-validation before writing to disk
384+
385+
These enhancements will be implemented as separate issues after the core functionality is complete.

0 commit comments

Comments
 (0)