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 project-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ frontends
getent
getopt
handleable
hashset
hetznercloud
hexdigit
hexdump
Expand Down
18 changes: 18 additions & 0 deletions src/application/command_handlers/create/config/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::path::PathBuf;
use thiserror::Error;

use crate::domain::EnvironmentNameError;
use crate::domain::ProfileNameError;
use crate::shared::UsernameError;

/// Errors that can occur during configuration validation
Expand All @@ -24,6 +25,10 @@ pub enum CreateConfigError {
#[error("Invalid SSH username: {0}")]
InvalidUsername(#[from] UsernameError),

/// Invalid profile name format
#[error("Invalid profile name: {0}")]
InvalidProfileName(#[from] ProfileNameError),

/// SSH private key file not found
#[error("SSH private key file not found: {path}")]
PrivateKeyNotFound { path: PathBuf },
Expand Down Expand Up @@ -110,6 +115,19 @@ impl CreateConfigError {
\n\
Fix: Update the SSH username in your configuration to follow Linux username requirements."
}
Self::InvalidProfileName(_) => {
"LXD profile name validation failed.\n\
\n\
Valid profile names must:\n\
- Be 1-63 characters long\n\
- Contain only ASCII letters, numbers, and dashes\n\
- Not start with a digit or dash\n\
- Not end with a dash\n\
\n\
Examples: 'torrust-profile', 'default', 'dev-profile'\n\
\n\
Fix: Update the profile_name in your provider configuration to follow these rules."
}
Self::PrivateKeyNotFound { .. } => {
"SSH private key file not found.\n\
\n\
Expand Down
9 changes: 9 additions & 0 deletions src/application/command_handlers/create/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@
//! - `SshCredentialsConfig` - SSH credentials configuration (config layer)
//! - `EnvironmentSection` - Environment-specific settings
//!
//! ### Provider Configuration
//!
//! Provider configuration is organized in the `provider` submodule:
//! - `ProviderSection` - Tagged enum for provider-specific settings
//! - `LxdProviderSection` - LXD provider configuration
//! - `HetznerProviderSection` - Hetzner provider configuration
//!
//! Note: `SshCredentialsConfig` (config layer) is distinct from
//! `adapters::ssh::SshCredentials` (adapter layer). The config version uses
//! strings for paths and usernames, while the adapter version uses domain types.
Expand Down Expand Up @@ -95,9 +102,11 @@

pub mod environment_config;
pub mod errors;
pub mod provider;
pub mod ssh_credentials_config;

// Re-export commonly used types for convenience
pub use environment_config::{EnvironmentCreationConfig, EnvironmentSection};
pub use errors::CreateConfigError;
pub use provider::{HetznerProviderSection, LxdProviderSection, ProviderSection};
pub use ssh_credentials_config::SshCredentialsConfig;
83 changes: 83 additions & 0 deletions src/application/command_handlers/create/config/provider/hetzner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//! Hetzner Provider Configuration Section (Application Layer)
//!
//! This module contains the configuration section for the Hetzner provider.
//! Uses raw `String` fields for JSON deserialization, which are then validated
//! when converting to domain types.

use serde::{Deserialize, Serialize};

/// Hetzner-specific configuration section
///
/// Uses raw `String` fields for JSON deserialization. Convert to domain
/// `HetznerConfig` via `ProviderSection::to_provider_config()`.
///
/// # Examples
///
/// ```rust
/// use torrust_tracker_deployer_lib::application::command_handlers::create::config::HetznerProviderSection;
///
/// let section = HetznerProviderSection {
/// api_token: "your-api-token".to_string(),
/// server_type: "cx22".to_string(),
/// location: "nbg1".to_string(),
/// };
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HetznerProviderSection {
/// Hetzner API token (raw string).
pub api_token: String,

/// Hetzner server type (e.g., "cx22", "cx32", "cpx11").
pub server_type: String,

/// Hetzner datacenter location (e.g., "fsn1", "nbg1", "hel1").
pub location: String,
}

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

fn create_hetzner_section() -> HetznerProviderSection {
HetznerProviderSection {
api_token: "token".to_string(),
server_type: "cx22".to_string(),
location: "nbg1".to_string(),
}
}

#[test]
fn it_should_serialize_to_json() {
let section = create_hetzner_section();
let json = serde_json::to_string(&section).unwrap();
assert!(json.contains("\"api_token\":\"token\""));
assert!(json.contains("\"server_type\":\"cx22\""));
assert!(json.contains("\"location\":\"nbg1\""));
}

#[test]
fn it_should_deserialize_from_json() {
let json = r#"{"api_token":"token","server_type":"cx22","location":"nbg1"}"#;
let section: HetznerProviderSection = serde_json::from_str(json).unwrap();
assert_eq!(section.api_token, "token");
assert_eq!(section.server_type, "cx22");
assert_eq!(section.location, "nbg1");
}

#[test]
fn it_should_be_cloneable() {
let section = create_hetzner_section();
let cloned = section.clone();
assert_eq!(section, cloned);
}

#[test]
fn it_should_implement_debug_trait() {
let section = create_hetzner_section();
let debug = format!("{section:?}");
assert!(debug.contains("HetznerProviderSection"));
assert!(debug.contains("api_token"));
assert!(debug.contains("server_type"));
assert!(debug.contains("location"));
}
}
67 changes: 67 additions & 0 deletions src/application/command_handlers/create/config/provider/lxd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//! LXD Provider Configuration Section (Application Layer)
//!
//! This module contains the configuration section for the LXD provider.
//! Uses raw `String` for JSON deserialization, which is then validated
//! when converting to domain types.

use serde::{Deserialize, Serialize};

/// LXD-specific configuration section
///
/// Uses raw `String` for JSON deserialization. Convert to domain `LxdConfig`
/// via `ProviderSection::to_provider_config()`.
///
/// # Examples
///
/// ```rust
/// use torrust_tracker_deployer_lib::application::command_handlers::create::config::LxdProviderSection;
///
/// let section = LxdProviderSection {
/// profile_name: "torrust-profile-dev".to_string(),
/// };
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LxdProviderSection {
/// LXD profile name (raw string - validated on conversion).
pub profile_name: String,
}

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

#[test]
fn it_should_serialize_to_json() {
let section = LxdProviderSection {
profile_name: "test".to_string(),
};
let json = serde_json::to_string(&section).unwrap();
assert!(json.contains("\"profile_name\":\"test\""));
}

#[test]
fn it_should_deserialize_from_json() {
let json = r#"{"profile_name":"torrust-profile"}"#;
let section: LxdProviderSection = serde_json::from_str(json).unwrap();
assert_eq!(section.profile_name, "torrust-profile");
}

#[test]
fn it_should_be_cloneable() {
let section = LxdProviderSection {
profile_name: "test".to_string(),
};
let cloned = section.clone();
assert_eq!(section, cloned);
}

#[test]
fn it_should_implement_debug_trait() {
let section = LxdProviderSection {
profile_name: "test".to_string(),
};
let debug = format!("{section:?}");
assert!(debug.contains("LxdProviderSection"));
assert!(debug.contains("profile_name"));
}
}
Loading
Loading