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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ anyhow = "1.0"
chrono = { version = "0.4", features = [ "serde" ] }
clap = { version = "4.0", features = [ "derive" ] }
derive_more = "0.99"
figment = { version = "0.10", features = [ "json" ] }
rust-embed = "8.0"
serde = { version = "1.0", features = [ "derive" ] }
serde_json = "1.0"
Expand Down
63 changes: 32 additions & 31 deletions project-words.txt
Original file line number Diff line number Diff line change
@@ -1,62 +1,81 @@
AAAAB
Avalonia
CIFS
Cockburn
Crossplane
Dockerfiles
EAAAADAQABAAABAQC
EPEL
Gossman
Hostnames
MAAACBA
MVVM
NOPASSWD
OAAAAN
Pulumi
RAII
RUSTDOCFLAGS
Repomix
Rustdoc
SCRIPTDIR
Scriptability
Silverlight
Subissue
Taplo
Tera
Testcontain
Testcontainers
Testinfra
Torrust
addgroup
adduser
appender
appendonly
architecting
autorestart
Avalonia
buildx
chdir
childlogdir
chkdsk
chrono
CIFS
clippy
clonable
cloneable
cloudinit
Cockburn
concepsts
connrefused
containerd
cpus
creds
Crossplane
custompass
customuser
dearmor
debootstrap
debuginfo
derefs
distutils
Dockerfiles
doctest
doctests
downcasted
downcasting
dpkg
dtolnay
EAAAADAQABAAABAQC
ehthumbs
elif
Émojis
endfor
endraw
epel
EPEL
eprintln
exitcode
getent
Gossman
handleable
hexdump
Hostnames
hotfixes
htdocs
hugepages
impls
journalctl
jsonlint
keepalive
keygen
keyrings
Expand All @@ -66,15 +85,13 @@ logfile
logicaldisk
loglevel
lxdbr
MAAACBA
maxbytes
mgmt
millis
mockall
mocksecret
mtorrust
multiprocess
MVVM
myapp
myenv
nameof
Expand All @@ -86,10 +103,8 @@ nocapture
noconfirm
nodaemon
noninteractive
NOPASSWD
nslookup
nullglob
OAAAAN
oneline
pacman
parameterizing
Expand All @@ -104,10 +119,7 @@ preconfigured
prereq
println
publickey
Pulumi
pytest
RAII
Repomix
reprovisioning
reqwest
resolv
Expand All @@ -118,25 +130,19 @@ rstest
runbooks
runcmd
rustc
Rustdoc
RUSTDOCFLAGS
rustflags
rustup
Scriptability
SCRIPTDIR
secureboot
serde
serverurl
shellcheck
Silverlight
smorimoto
spëcial
spki
spëcial
sqlx
sshpass
startretries
stringly
Subissue
subissues
subshell
substates
Expand All @@ -145,18 +151,12 @@ supervisord
swappability
sysfs
sysv
Taplo
taskkill
tasklist
Tera
terraformrc
tést
Testcontain
testcontainer
testcontainers
Testcontainers
testhost
Testinfra
testkey
testpass
testuser
Expand All @@ -168,7 +168,7 @@ tlsv
tmpfiles
tmpfs
torrust
Torrust
tést
unergonomic
unrepresentable
unsubscription
Expand All @@ -180,6 +180,7 @@ vbqajnc
viewmodel
webservers
writeln
Émojis
значение
ключ
конфиг
Expand Down
2 changes: 1 addition & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ pub fn run() {

match cli.command {
Some(command) => {
if let Err(e) = presentation::execute(command) {
if let Err(e) = presentation::execute(command, &cli.global.working_dir) {
presentation::handle_error(&e);
std::process::exit(1);
}
Expand Down
15 changes: 15 additions & 0 deletions src/presentation/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@ pub struct GlobalArgs {
/// observability and the application cannot function without it.
#[arg(long, default_value = "./data/logs", global = true)]
pub log_dir: PathBuf,

/// Working directory for environment data (default: .)
///
/// Root directory where environment data will be stored. Each environment
/// creates subdirectories within this location for build files and state.
/// This is useful for testing or when you want to manage environments in
/// a different location than the current directory.
///
/// Examples:
/// - Default: './data' (relative to current directory)
/// - Testing: '/tmp/test-workspace' (absolute path)
/// - Production: '/var/lib/torrust-deployer' (system location)
#[arg(long, default_value = ".", global = true)]
pub working_dir: PathBuf,
}

impl GlobalArgs {
Expand All @@ -81,6 +95,7 @@ impl GlobalArgs {
/// log_stderr_format: LogFormat::Pretty,
/// log_output: LogOutput::FileAndStderr,
/// log_dir: PathBuf::from("/tmp/logs"),
/// working_dir: PathBuf::from("."),
/// };
/// let config = args.logging_config();
/// // config will have specified log formats and directory
Expand Down
17 changes: 17 additions & 0 deletions src/presentation/cli/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,29 @@

use clap::Subcommand;

use std::path::PathBuf;

/// Available CLI commands
///
/// This enum defines all the subcommands available in the CLI application.
/// Each variant represents a specific operation that can be performed.
#[derive(Subcommand, Debug)]
pub enum Commands {
/// Create a new deployment environment
///
/// 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.
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,
},

/// Destroy an existing deployment environment
///
/// This command will tear down all infrastructure associated with the
Expand Down
98 changes: 98 additions & 0 deletions src/presentation/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ mod tests {
Commands::Destroy { environment } => {
assert_eq!(environment, "test-env");
}
Commands::Create { .. } => panic!("Expected Destroy command"),
}
}

Expand All @@ -62,6 +63,7 @@ mod tests {
Commands::Destroy { environment } => {
assert_eq!(environment, env_name);
}
Commands::Create { .. } => panic!("Expected Destroy command"),
}
}
}
Expand Down Expand Up @@ -102,6 +104,7 @@ mod tests {
Commands::Destroy { environment } => {
assert_eq!(environment, "test-env");
}
Commands::Create { .. } => panic!("Expected Destroy command"),
}

// Log options are set but we don't compare them as they don't implement PartialEq
Expand Down Expand Up @@ -163,4 +166,99 @@ mod tests {
"Help text should mention environment parameter"
);
}

#[test]
fn it_should_parse_create_subcommand() {
let args = vec![
"torrust-tracker-deployer",
"create",
"--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::Destroy { .. } => panic!("Expected Create command"),
}
}

#[test]
fn it_should_parse_create_with_short_flag() {
let args = vec!["torrust-tracker-deployer", "create", "-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::Destroy { .. } => panic!("Expected Create command"),
}
}

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

assert!(result.is_err());
let error = result.unwrap_err();
let error_message = error.to_string();
assert!(
error_message.contains("required") || error_message.contains("--env-file"),
"Error message should indicate missing required --env-file: {error_message}"
);
}

#[test]
fn it_should_parse_working_dir_global_option() {
let args = vec![
"torrust-tracker-deployer",
"--working-dir",
"/tmp/workspace",
"create",
"--env-file",
"config.json",
];
let cli = Cli::try_parse_from(args).unwrap();

assert_eq!(
cli.global.working_dir,
std::path::PathBuf::from("/tmp/workspace")
);

match cli.command.unwrap() {
Commands::Create { env_file } => {
assert_eq!(env_file, std::path::PathBuf::from("config.json"));
}
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 cli = Cli::try_parse_from(args).unwrap();

assert_eq!(cli.global.working_dir, std::path::PathBuf::from("."));
}

#[test]
fn it_should_show_create_help() {
let args = vec!["torrust-tracker-deployer", "create", "--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"
);
}
}
Loading
Loading