Skip to content

Commit 0eff71a

Browse files
committed
Merge #49: Add template subcommand for create command
48dd8a0 fix: resolve clippy linting warnings (copilot-swe-agent[bot]) 6e19205 style: apply rustfmt formatting (copilot-swe-agent[bot]) f277803 feat: [#41] add template subcommand for create command (copilot-swe-agent[bot]) 2fe4bf1 Initial plan (copilot-swe-agent[bot]) Pull request description: ## Implementation Complete: Template Generation CLI Support (Issue #41) This PR implements the `template` subcommand for the create command, allowing users to generate configuration file templates via the CLI. ### Implementation Checklist ✅ - [x] Update CLI command structure to support `create` subcommands (environment/template) - [x] Add `CreateAction` enum with `Environment` and `Template` variants - [x] Implement `handle_template_generation()` handler that calls domain method - [x] Add user-friendly success messages with next steps - [x] Update command routing to handle new subcommand structure - [x] Add unit tests for CLI argument parsing - [x] Add integration tests for template generation via CLI - [x] Update CLI module tests to reflect new command structure - [x] Manual testing of CLI commands - [x] Linting and formatting (clippy and rustfmt) - [x] Code review (passed with no comments) - [x] Fix clippy pedantic warnings ### Security Summary **No new security vulnerabilities introduced.** This implementation is a thin CLI wrapper that: - Delegates to existing domain method `EnvironmentCreationConfig::generate_template_file()` - Uses standard async runtime for file operations - Properly propagates domain-layer errors with `.help()` methods - Creates parent directories safely using `tokio::fs::create_dir_all()` - Writes files safely using `tokio::fs::write()` The security posture is maintained by: - No raw file operations - delegates to tokio's safe async file APIs - No user input validation required (paths are already validated) - Error handling includes proper context and actionable guidance - No secrets or sensitive data in template generation ### Test Results - **971 tests pass** (4 new template tests + 967 existing) - **Clippy**: ✅ No warnings (all pedantic lints pass) - **Rustfmt**: All code formatted - **Manual testing**: All CLI commands work as expected ### CLI Usage Examples ```bash # Generate template with default path $ torrust-tracker-deployer create template ⏳ Generating configuration template... ✅ Configuration template generated: environment-template.json # Generate template with custom path $ torrust-tracker-deployer create template ./config/my-env.json ✅ Configuration template generated: ./config/my-env.json # Create environment from config $ torrust-tracker-deployer create environment --env-file config.json ``` ### Closes - Issue #41 <!-- START COPILOT CODING AGENT SUFFIX --> <details> <summary>Original prompt</summary> ---- *This section details on the original issue you should resolve* <issue_title>[Subissue 7/7] Template Generation Support (Optional)</issue_title> <issue_description>**Parent Epic**: #34 - Implement Create Environment Command **Depends On**: #40 - Template System Integration **Status**: OPTIONAL ENHANCEMENT **Estimated Time**: 1-2 hours > ⚠️ **Note**: This is an optional enhancement that can be deferred. It requires #40 (Template System Integration) to be completed first. ## Overview Implement the `template` subcommand for the create command, allowing users to generate configuration file templates. This enhances user experience by providing a starting point for configuration creation. ## Goals - [ ] Implement `template` subcommand in the create command structure - [ ] Generate JSON configuration template files - [ ] Support custom output paths for template generation - [ ] Provide helpful placeholder content with clear replacement instructions - [ ] Integrate with existing CLI subcommand architecture ## Architecture Requirements **DDD Layer**: Presentation (CLI interface) + Infrastructure (template generation) **Module Path**: `src/presentation/console/subcommands/create/` + `src/infrastructure/templates/` **Pattern**: CLI Subcommand + Template Generation **Dependencies**: Requires #40 (Template System Integration) to be completed first. ## CLI Interface ```bash # Generate template configuration file (JSON format) torrust-tracker-deployer create template # Creates: ./environment-template.json in current working directory # Generate template in specific directory torrust-tracker-deployer create template ./config/environment.json # Generate template with custom filename torrust-tracker-deployer create template ./my-environment.json # Show help for template subcommand torrust-tracker-deployer create template --help ``` ## Template Example ```json { "environment": { "name": "my-environment" }, "ssh_credentials": { "private_key_path": "~/.ssh/id_rsa", "public_key_path": "~/.ssh/id_rsa.pub", "username": "torrust", "port": 22 } } ``` ## Acceptance Criteria - [ ] `create template` subcommand generates valid JSON template - [ ] Template includes helpful placeholder values - [ ] Template includes comments explaining each field (if JSON supports it, or in separate docs) - [ ] Custom output paths are supported - [ ] Default filename is `environment-template.json` - [ ] Error handling follows tiered help system pattern - [ ] Help text provides clear usage examples ## Implementation Notes - Extend existing CLI subcommand structure from Subissue 3 - Use TemplateManager from Subissue 6 for template generation - Follow existing patterns for CLI subcommands - Ensure template content matches environment configuration schema For detailed specification, see: [docs/issues/epic-create-environment-command-subissue-7-template-generation-support.md](https://github.com/torrust/torrust-tracker-deployer/blob/main/docs/issues/epic-create-environment-command-subissue-7-template-generation-support.md)</issue_description> <agent_instructions>@copilot Please implement this issue using the v2 specification: 📋 Use this spec: docs/issues/41-subissue-7-template-generation-support-v2.md ⚠️ Important: The v1 spec is outdated. PR #48 already implemented template generation in the domain layer (EnvironmentCreationConfig::generate_template_file()). This is a simple CLI integration task - just add the template subcommand and call the existing domain method. Don't create new template generation logic or infrastructure modules.</agent_instructions> ## Comments on the Issue (you are @copilot in this section) <comments> <comment_new><author>@josecelano</author><body> ## ✅ Issue Documentation Updated The issue specification has been updated to reflect the implementation completed in PR #48. ### Key Changes **Previous specification was outdated** - it was written before PR #48 implemented struct-based template generation in the domain layer. **New v2 specification**: [docs/issues/41-subissue-7-template-generation-support-v2.md](https://github.com/torrust/torrust-tracker-deployer/blob/main/docs/issues/41-subissue-7-template-generation-support-v2.md) ### What Changed 1. **Architecture**: Domain layer (not infrastructure) - template generation already implemented 2. **Implementation scope**: Thin CLI wrapper only (1-2 hours of work) 3. **API**: Use existing `EnvironmentCreationConfig::generate_template_file()` method 4. **Error handling**: Use existing `CreateConfigError` enum 5. **Testing**: Focus on CLI integration tests (domain logic already tested) ### What's Already Done ✅ - Template generation logic in `src/domain/config/environment_config.rs` - Error handling with `.help()` methods - Comprehens... </details> - Fixes #41 <!-- START COPILOT CODING AGENT TIPS --> --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). ACKs for top commit: josecelano: ACK 48dd8a0 Tree-SHA512: 498c144970e8c4d8371f9e0a97918bd04f47ac0ffadc66df0b9a08e58227ca158fb0054d0ea752749cc9722fbc47b091d463798da3d78e155b18f8be98411644
2 parents 84994a1 + 48dd8a0 commit 0eff71a

File tree

9 files changed

+461
-69
lines changed

9 files changed

+461
-69
lines changed

src/presentation/cli/commands.rs

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,13 @@ use std::path::PathBuf;
1313
/// Each variant represents a specific operation that can be performed.
1414
#[derive(Subcommand, Debug)]
1515
pub enum Commands {
16-
/// Create a new deployment environment
16+
/// Create operations (environment creation or template generation)
1717
///
18-
/// This command creates a new environment based on a configuration file.
19-
/// The configuration file specifies the environment name, SSH credentials,
20-
/// and other settings required for environment creation.
18+
/// This command provides subcommands for creating environments and generating
19+
/// configuration templates.
2120
Create {
22-
/// Path to the environment configuration file
23-
///
24-
/// The configuration file must be in JSON format and contain all
25-
/// required fields for environment creation. Use --help for more
26-
/// information about the configuration format.
27-
#[arg(long, short = 'f', value_name = "FILE")]
28-
env_file: PathBuf,
21+
#[command(subcommand)]
22+
action: CreateAction,
2923
},
3024

3125
/// Destroy an existing deployment environment
@@ -65,3 +59,56 @@ pub enum Commands {
6559
// version: String,
6660
// },
6761
}
62+
63+
/// Actions available for the create command
64+
#[derive(Debug, Subcommand)]
65+
pub enum CreateAction {
66+
/// Create environment from configuration file
67+
///
68+
/// This subcommand creates a new deployment environment based on a
69+
/// configuration file. The configuration file specifies the environment
70+
/// name, SSH credentials, and other settings required for creation.
71+
Environment {
72+
/// Path to the environment configuration file
73+
///
74+
/// The configuration file must be in JSON format and contain all
75+
/// required fields for environment creation.
76+
#[arg(long, short = 'f', value_name = "FILE")]
77+
env_file: PathBuf,
78+
},
79+
80+
/// Generate template configuration file
81+
///
82+
/// This subcommand generates a JSON configuration template file with
83+
/// placeholder values. Edit the template to provide your actual
84+
/// configuration values, then use 'create environment' to create
85+
/// the environment.
86+
Template {
87+
/// Output path for the template file (optional)
88+
///
89+
/// If not provided, creates environment-template.json in the
90+
/// current working directory. Parent directories will be created
91+
/// automatically if they don't exist.
92+
#[arg(value_name = "PATH")]
93+
output_path: Option<PathBuf>,
94+
},
95+
}
96+
97+
impl CreateAction {
98+
/// Get the default template output path
99+
#[must_use]
100+
pub fn default_template_path() -> PathBuf {
101+
PathBuf::from("environment-template.json")
102+
}
103+
}
104+
105+
#[cfg(test)]
106+
mod tests {
107+
use super::*;
108+
109+
#[test]
110+
fn it_should_use_default_template_path() {
111+
let default_path = CreateAction::default_template_path();
112+
assert_eq!(default_path, PathBuf::from("environment-template.json"));
113+
}
114+
}

src/presentation/cli/mod.rs

Lines changed: 127 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub mod args;
1111
pub mod commands;
1212

1313
pub use args::GlobalArgs;
14-
pub use commands::Commands;
14+
pub use commands::{Commands, CreateAction};
1515

1616
/// Command-line interface for Torrust Tracker Deployer
1717
///
@@ -168,40 +168,57 @@ mod tests {
168168
}
169169

170170
#[test]
171-
fn it_should_parse_create_subcommand() {
171+
fn it_should_parse_create_environment_subcommand() {
172172
let args = vec![
173173
"torrust-tracker-deployer",
174174
"create",
175+
"environment",
175176
"--env-file",
176177
"config.json",
177178
];
178179
let cli = Cli::try_parse_from(args).unwrap();
179180

180181
assert!(cli.command.is_some());
181182
match cli.command.unwrap() {
182-
Commands::Create { env_file } => {
183-
assert_eq!(env_file, std::path::PathBuf::from("config.json"));
184-
}
183+
Commands::Create { action } => match action {
184+
crate::presentation::cli::CreateAction::Environment { env_file } => {
185+
assert_eq!(env_file, std::path::PathBuf::from("config.json"));
186+
}
187+
crate::presentation::cli::CreateAction::Template { .. } => {
188+
panic!("Expected Environment action")
189+
}
190+
},
185191
Commands::Destroy { .. } => panic!("Expected Create command"),
186192
}
187193
}
188194

189195
#[test]
190-
fn it_should_parse_create_with_short_flag() {
191-
let args = vec!["torrust-tracker-deployer", "create", "-f", "env.json"];
196+
fn it_should_parse_create_environment_with_short_flag() {
197+
let args = vec![
198+
"torrust-tracker-deployer",
199+
"create",
200+
"environment",
201+
"-f",
202+
"env.json",
203+
];
192204
let cli = Cli::try_parse_from(args).unwrap();
193205

194206
match cli.command.unwrap() {
195-
Commands::Create { env_file } => {
196-
assert_eq!(env_file, std::path::PathBuf::from("env.json"));
197-
}
207+
Commands::Create { action } => match action {
208+
crate::presentation::cli::CreateAction::Environment { env_file } => {
209+
assert_eq!(env_file, std::path::PathBuf::from("env.json"));
210+
}
211+
crate::presentation::cli::CreateAction::Template { .. } => {
212+
panic!("Expected Environment action")
213+
}
214+
},
198215
Commands::Destroy { .. } => panic!("Expected Create command"),
199216
}
200217
}
201218

202219
#[test]
203-
fn it_should_require_env_file_parameter_for_create() {
204-
let args = vec!["torrust-tracker-deployer", "create"];
220+
fn it_should_require_env_file_parameter_for_create_environment() {
221+
let args = vec!["torrust-tracker-deployer", "create", "environment"];
205222
let result = Cli::try_parse_from(args);
206223

207224
assert!(result.is_err());
@@ -214,12 +231,13 @@ mod tests {
214231
}
215232

216233
#[test]
217-
fn it_should_parse_working_dir_global_option() {
234+
fn it_should_parse_working_dir_global_option_with_create_environment() {
218235
let args = vec![
219236
"torrust-tracker-deployer",
220237
"--working-dir",
221238
"/tmp/workspace",
222239
"create",
240+
"environment",
223241
"--env-file",
224242
"config.json",
225243
];
@@ -231,16 +249,27 @@ mod tests {
231249
);
232250

233251
match cli.command.unwrap() {
234-
Commands::Create { env_file } => {
235-
assert_eq!(env_file, std::path::PathBuf::from("config.json"));
236-
}
252+
Commands::Create { action } => match action {
253+
crate::presentation::cli::CreateAction::Environment { env_file } => {
254+
assert_eq!(env_file, std::path::PathBuf::from("config.json"));
255+
}
256+
crate::presentation::cli::CreateAction::Template { .. } => {
257+
panic!("Expected Environment action")
258+
}
259+
},
237260
Commands::Destroy { .. } => panic!("Expected Create command"),
238261
}
239262
}
240263

241264
#[test]
242265
fn it_should_use_default_working_dir_when_not_specified() {
243-
let args = vec!["torrust-tracker-deployer", "create", "-f", "config.json"];
266+
let args = vec![
267+
"torrust-tracker-deployer",
268+
"create",
269+
"environment",
270+
"-f",
271+
"config.json",
272+
];
244273
let cli = Cli::try_parse_from(args).unwrap();
245274

246275
assert_eq!(cli.global.working_dir, std::path::PathBuf::from("."));
@@ -255,10 +284,91 @@ mod tests {
255284
let error = result.unwrap_err();
256285
assert_eq!(error.kind(), clap::error::ErrorKind::DisplayHelp);
257286

287+
let help_text = error.to_string();
288+
assert!(
289+
help_text.contains("environment") || help_text.contains("template"),
290+
"Help text should mention subcommands: {help_text}"
291+
);
292+
}
293+
294+
#[test]
295+
fn it_should_parse_create_template_without_path() {
296+
let args = vec!["torrust-tracker-deployer", "create", "template"];
297+
let cli = Cli::try_parse_from(args).unwrap();
298+
299+
match cli.command.unwrap() {
300+
Commands::Create { action } => match action {
301+
crate::presentation::cli::CreateAction::Template { output_path } => {
302+
assert!(output_path.is_none());
303+
}
304+
crate::presentation::cli::CreateAction::Environment { .. } => {
305+
panic!("Expected Template action")
306+
}
307+
},
308+
Commands::Destroy { .. } => panic!("Expected Create command"),
309+
}
310+
}
311+
312+
#[test]
313+
fn it_should_parse_create_template_with_custom_path() {
314+
let args = vec![
315+
"torrust-tracker-deployer",
316+
"create",
317+
"template",
318+
"./config/my-env.json",
319+
];
320+
let cli = Cli::try_parse_from(args).unwrap();
321+
322+
match cli.command.unwrap() {
323+
Commands::Create { action } => match action {
324+
crate::presentation::cli::CreateAction::Template { output_path } => {
325+
assert_eq!(
326+
output_path,
327+
Some(std::path::PathBuf::from("./config/my-env.json"))
328+
);
329+
}
330+
crate::presentation::cli::CreateAction::Environment { .. } => {
331+
panic!("Expected Template action")
332+
}
333+
},
334+
Commands::Destroy { .. } => panic!("Expected Create command"),
335+
}
336+
}
337+
338+
#[test]
339+
fn it_should_show_create_environment_help() {
340+
let args = vec![
341+
"torrust-tracker-deployer",
342+
"create",
343+
"environment",
344+
"--help",
345+
];
346+
let result = Cli::try_parse_from(args);
347+
348+
assert!(result.is_err());
349+
let error = result.unwrap_err();
350+
assert_eq!(error.kind(), clap::error::ErrorKind::DisplayHelp);
351+
258352
let help_text = error.to_string();
259353
assert!(
260354
help_text.contains("env-file") || help_text.contains("configuration"),
261355
"Help text should mention env-file parameter"
262356
);
263357
}
358+
359+
#[test]
360+
fn it_should_show_create_template_help() {
361+
let args = vec!["torrust-tracker-deployer", "create", "template", "--help"];
362+
let result = Cli::try_parse_from(args);
363+
364+
assert!(result.is_err());
365+
let error = result.unwrap_err();
366+
assert_eq!(error.kind(), clap::error::ErrorKind::DisplayHelp);
367+
368+
let help_text = error.to_string();
369+
assert!(
370+
help_text.contains("template") || help_text.contains("placeholder"),
371+
"Help text should mention template generation"
372+
);
373+
}
264374
}

src/presentation/commands/create/errors.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ pub enum CreateSubcommandError {
6565
#[source]
6666
CreateCommandHandlerError,
6767
),
68+
69+
/// Template generation failed
70+
#[error("Template generation failed")]
71+
TemplateGenerationFailed(
72+
/// Underlying template generation error from domain layer
73+
#[source]
74+
crate::domain::config::CreateConfigError,
75+
),
6876
}
6977

7078
impl CreateSubcommandError {
@@ -100,7 +108,7 @@ impl CreateSubcommandError {
100108
4. Use absolute paths or paths relative to current directory
101109
102110
Example:
103-
torrust-tracker-deployer create --env-file ./config/environment.json
111+
torrust-tracker-deployer create environment --env-file ./config/environment.json
104112
105113
For more information about configuration format, see the documentation."
106114
}
@@ -142,7 +150,9 @@ Example valid configuration:
142150
For more information, see the configuration documentation."
143151
}
144152
},
145-
Self::ConfigValidationFailed(inner) => inner.help(),
153+
Self::ConfigValidationFailed(inner) | Self::TemplateGenerationFailed(inner) => {
154+
inner.help()
155+
}
146156
Self::CommandFailed(inner) => inner.help(),
147157
}
148158
}

src/presentation/commands/create/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,4 @@ mod tests;
4242
// Re-export commonly used types for convenience
4343
pub use config_loader::ConfigLoader;
4444
pub use errors::{ConfigFormat, CreateSubcommandError};
45-
pub use subcommand::handle;
45+
pub use subcommand::handle_create_command;

0 commit comments

Comments
 (0)