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
69 changes: 58 additions & 11 deletions src/presentation/cli/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,13 @@ use std::path::PathBuf;
/// Each variant represents a specific operation that can be performed.
#[derive(Subcommand, Debug)]
pub enum Commands {
/// Create a new deployment environment
/// Create operations (environment creation or template generation)
///
/// This command creates a new environment based on a configuration file.
/// The configuration file specifies the environment name, SSH credentials,
/// and other settings required for environment creation.
/// This command provides subcommands for creating environments and generating
/// configuration templates.
Create {
/// Path to the environment configuration file
///
/// The configuration file must be in JSON format and contain all
/// required fields for environment creation. Use --help for more
/// information about the configuration format.
#[arg(long, short = 'f', value_name = "FILE")]
env_file: PathBuf,
#[command(subcommand)]
action: CreateAction,
},

/// Destroy an existing deployment environment
Expand Down Expand Up @@ -65,3 +59,56 @@ pub enum Commands {
// version: String,
// },
}

/// Actions available for the create command
#[derive(Debug, Subcommand)]
pub enum CreateAction {
/// Create environment from configuration file
///
/// This subcommand creates a new deployment environment based on a
/// configuration file. The configuration file specifies the environment
/// name, SSH credentials, and other settings required for creation.
Environment {
/// Path to the environment configuration file
///
/// The configuration file must be in JSON format and contain all
/// required fields for environment creation.
#[arg(long, short = 'f', value_name = "FILE")]
env_file: PathBuf,
},

/// Generate template configuration file
///
/// This subcommand generates a JSON configuration template file with
/// placeholder values. Edit the template to provide your actual
/// configuration values, then use 'create environment' to create
/// the environment.
Template {
/// Output path for the template file (optional)
///
/// If not provided, creates environment-template.json in the
/// current working directory. Parent directories will be created
/// automatically if they don't exist.
#[arg(value_name = "PATH")]
output_path: Option<PathBuf>,
},
}

impl CreateAction {
/// Get the default template output path
#[must_use]
pub fn default_template_path() -> PathBuf {
PathBuf::from("environment-template.json")
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_should_use_default_template_path() {
let default_path = CreateAction::default_template_path();
assert_eq!(default_path, PathBuf::from("environment-template.json"));
}
}
144 changes: 127 additions & 17 deletions src/presentation/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub mod args;
pub mod commands;

pub use args::GlobalArgs;
pub use commands::Commands;
pub use commands::{Commands, CreateAction};

/// Command-line interface for Torrust Tracker Deployer
///
Expand Down Expand Up @@ -168,40 +168,57 @@ mod tests {
}

#[test]
fn it_should_parse_create_subcommand() {
fn it_should_parse_create_environment_subcommand() {
let args = vec![
"torrust-tracker-deployer",
"create",
"environment",
"--env-file",
"config.json",
];
let cli = Cli::try_parse_from(args).unwrap();

assert!(cli.command.is_some());
match cli.command.unwrap() {
Commands::Create { env_file } => {
assert_eq!(env_file, std::path::PathBuf::from("config.json"));
}
Commands::Create { action } => match action {
crate::presentation::cli::CreateAction::Environment { env_file } => {
assert_eq!(env_file, std::path::PathBuf::from("config.json"));
}
crate::presentation::cli::CreateAction::Template { .. } => {
panic!("Expected Environment action")
}
},
Commands::Destroy { .. } => panic!("Expected Create command"),
}
}

#[test]
fn it_should_parse_create_with_short_flag() {
let args = vec!["torrust-tracker-deployer", "create", "-f", "env.json"];
fn it_should_parse_create_environment_with_short_flag() {
let args = vec![
"torrust-tracker-deployer",
"create",
"environment",
"-f",
"env.json",
];
let cli = Cli::try_parse_from(args).unwrap();

match cli.command.unwrap() {
Commands::Create { env_file } => {
assert_eq!(env_file, std::path::PathBuf::from("env.json"));
}
Commands::Create { action } => match action {
crate::presentation::cli::CreateAction::Environment { env_file } => {
assert_eq!(env_file, std::path::PathBuf::from("env.json"));
}
crate::presentation::cli::CreateAction::Template { .. } => {
panic!("Expected Environment action")
}
},
Commands::Destroy { .. } => panic!("Expected Create command"),
}
}

#[test]
fn it_should_require_env_file_parameter_for_create() {
let args = vec!["torrust-tracker-deployer", "create"];
fn it_should_require_env_file_parameter_for_create_environment() {
let args = vec!["torrust-tracker-deployer", "create", "environment"];
let result = Cli::try_parse_from(args);

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

#[test]
fn it_should_parse_working_dir_global_option() {
fn it_should_parse_working_dir_global_option_with_create_environment() {
let args = vec![
"torrust-tracker-deployer",
"--working-dir",
"/tmp/workspace",
"create",
"environment",
"--env-file",
"config.json",
];
Expand All @@ -231,16 +249,27 @@ mod tests {
);

match cli.command.unwrap() {
Commands::Create { env_file } => {
assert_eq!(env_file, std::path::PathBuf::from("config.json"));
}
Commands::Create { action } => match action {
crate::presentation::cli::CreateAction::Environment { env_file } => {
assert_eq!(env_file, std::path::PathBuf::from("config.json"));
}
crate::presentation::cli::CreateAction::Template { .. } => {
panic!("Expected Environment action")
}
},
Commands::Destroy { .. } => panic!("Expected Create command"),
}
}

#[test]
fn it_should_use_default_working_dir_when_not_specified() {
let args = vec!["torrust-tracker-deployer", "create", "-f", "config.json"];
let args = vec![
"torrust-tracker-deployer",
"create",
"environment",
"-f",
"config.json",
];
let cli = Cli::try_parse_from(args).unwrap();

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

let help_text = error.to_string();
assert!(
help_text.contains("environment") || help_text.contains("template"),
"Help text should mention subcommands: {help_text}"
);
}

#[test]
fn it_should_parse_create_template_without_path() {
let args = vec!["torrust-tracker-deployer", "create", "template"];
let cli = Cli::try_parse_from(args).unwrap();

match cli.command.unwrap() {
Commands::Create { action } => match action {
crate::presentation::cli::CreateAction::Template { output_path } => {
assert!(output_path.is_none());
}
crate::presentation::cli::CreateAction::Environment { .. } => {
panic!("Expected Template action")
}
},
Commands::Destroy { .. } => panic!("Expected Create command"),
}
}

#[test]
fn it_should_parse_create_template_with_custom_path() {
let args = vec![
"torrust-tracker-deployer",
"create",
"template",
"./config/my-env.json",
];
let cli = Cli::try_parse_from(args).unwrap();

match cli.command.unwrap() {
Commands::Create { action } => match action {
crate::presentation::cli::CreateAction::Template { output_path } => {
assert_eq!(
output_path,
Some(std::path::PathBuf::from("./config/my-env.json"))
);
}
crate::presentation::cli::CreateAction::Environment { .. } => {
panic!("Expected Template action")
}
},
Commands::Destroy { .. } => panic!("Expected Create command"),
}
}

#[test]
fn it_should_show_create_environment_help() {
let args = vec![
"torrust-tracker-deployer",
"create",
"environment",
"--help",
];
let result = Cli::try_parse_from(args);

assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.kind(), clap::error::ErrorKind::DisplayHelp);

let help_text = error.to_string();
assert!(
help_text.contains("env-file") || help_text.contains("configuration"),
"Help text should mention env-file parameter"
);
}

#[test]
fn it_should_show_create_template_help() {
let args = vec!["torrust-tracker-deployer", "create", "template", "--help"];
let result = Cli::try_parse_from(args);

assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.kind(), clap::error::ErrorKind::DisplayHelp);

let help_text = error.to_string();
assert!(
help_text.contains("template") || help_text.contains("placeholder"),
"Help text should mention template generation"
);
}
}
14 changes: 12 additions & 2 deletions src/presentation/commands/create/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ pub enum CreateSubcommandError {
#[source]
CreateCommandHandlerError,
),

/// Template generation failed
#[error("Template generation failed")]
TemplateGenerationFailed(
/// Underlying template generation error from domain layer
#[source]
crate::domain::config::CreateConfigError,
),
}

impl CreateSubcommandError {
Expand Down Expand Up @@ -100,7 +108,7 @@ impl CreateSubcommandError {
4. Use absolute paths or paths relative to current directory

Example:
torrust-tracker-deployer create --env-file ./config/environment.json
torrust-tracker-deployer create environment --env-file ./config/environment.json

For more information about configuration format, see the documentation."
}
Expand Down Expand Up @@ -142,7 +150,9 @@ Example valid configuration:
For more information, see the configuration documentation."
}
},
Self::ConfigValidationFailed(inner) => inner.help(),
Self::ConfigValidationFailed(inner) | Self::TemplateGenerationFailed(inner) => {
inner.help()
}
Self::CommandFailed(inner) => inner.help(),
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/presentation/commands/create/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ mod tests;
// Re-export commonly used types for convenience
pub use config_loader::ConfigLoader;
pub use errors::{ConfigFormat, CreateSubcommandError};
pub use subcommand::handle;
pub use subcommand::handle_create_command;
Loading
Loading