Skip to content

Commit a8bcc2c

Browse files
committed
refactor: replace primitive String/&str with InstanceName type for LXD instance names
- Update LXD client methods to use &InstanceName parameters - Update template variables context to use InstanceName field - Update TofuTemplateRenderer to use InstanceName throughout - Add Serialize trait to InstanceName for JSON template rendering - Maintain backward compatibility with hardcoded values - Add TODO comments for Phase 3 configurability - Improve type safety with compile-time validation
1 parent 34c0f17 commit a8bcc2c

File tree

8 files changed

+77
-45
lines changed

8 files changed

+77
-45
lines changed

src/command_wrappers/lxd/client.rs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ use tracing::info;
2121

2222
use crate::command::CommandExecutor;
2323

24-
use super::instance::InstanceInfo;
24+
#[allow(unused_imports)]
25+
use super::instance::{InstanceInfo, InstanceName};
2526
use super::json_parser::LxdJsonParser;
2627

2728
/// A specialized LXD client for instance management.
@@ -67,7 +68,7 @@ impl LxdClient {
6768
/// This function will return an error if:
6869
/// * LXD command execution fails
6970
/// * JSON parsing fails
70-
pub fn get_instance_ip(&self, instance_name: &str) -> Result<Option<IpAddr>> {
71+
pub fn get_instance_ip(&self, instance_name: &InstanceName) -> Result<Option<IpAddr>> {
7172
info!("Getting IP address for instance: {}", instance_name);
7273

7374
let Some(instance) = self.get_instance_by_name(instance_name)? else {
@@ -108,7 +109,7 @@ impl LxdClient {
108109
/// * JSON parsing fails
109110
pub fn wait_for_instance_ip(
110111
&self,
111-
instance_name: &str,
112+
instance_name: &InstanceName,
112113
timeout_seconds: u64,
113114
poll_interval_seconds: u64,
114115
) -> Result<IpAddr> {
@@ -162,14 +163,17 @@ impl LxdClient {
162163
/// This function will return an error if:
163164
/// * LXD command execution fails
164165
/// * JSON parsing fails
165-
pub fn get_instance_by_name(&self, instance_name: &str) -> Result<Option<InstanceInfo>> {
166+
pub fn get_instance_by_name(
167+
&self,
168+
instance_name: &InstanceName,
169+
) -> Result<Option<InstanceInfo>> {
166170
info!("Getting instance by name: {}", instance_name);
167171

168172
let instances = self.list(Some(instance_name))?;
169173

170174
Ok(instances
171175
.into_iter()
172-
.find(|inst| inst.name.as_str() == instance_name))
176+
.find(|inst| inst.name.as_str() == instance_name.as_str()))
173177
}
174178

175179
/// List instances in JSON format
@@ -188,13 +192,13 @@ impl LxdClient {
188192
/// * The LXD command fails
189193
/// * LXD is not installed or accessible
190194
/// * JSON parsing fails
191-
fn list(&self, instance_name: Option<&str>) -> Result<Vec<InstanceInfo>> {
195+
fn list(&self, instance_name: Option<&InstanceName>) -> Result<Vec<InstanceInfo>> {
192196
info!("Listing LXD instances");
193197

194198
let mut args = vec!["list", "--format=json"];
195199

196200
if let Some(name) = instance_name {
197-
args.push(name);
201+
args.push(name.as_str());
198202
info!("Filtering by instance name: {}", name);
199203
}
200204

@@ -223,10 +227,10 @@ impl LxdClient {
223227
/// This function will return an error if:
224228
/// * The LXD command fails with an unexpected error
225229
/// * LXD is not installed or accessible
226-
pub fn delete_instance(&self, instance_name: &str, force: bool) -> Result<()> {
230+
pub fn delete_instance(&self, instance_name: &InstanceName, force: bool) -> Result<()> {
227231
info!("Deleting LXD instance: {}", instance_name);
228232

229-
let mut args = vec!["delete", instance_name];
233+
let mut args = vec!["delete", instance_name.as_str()];
230234
if force {
231235
args.push("--force");
232236
}

src/command_wrappers/lxd/instance/name.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ use std::fmt;
1818
use std::str::FromStr;
1919

2020
use anyhow::{anyhow, Result};
21+
#[allow(unused_imports)]
22+
use serde::Serialize;
2123

2224
/// A validated LXD instance name following LXD naming requirements.
2325
///
@@ -29,7 +31,7 @@ use anyhow::{anyhow, Result};
2931
///
3032
/// These requirements ensure that the instance name can be used in DNS records,
3133
/// on the file system, in various security profiles and as the host name.
32-
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
34+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
3335
pub struct InstanceName(String);
3436

3537
impl InstanceName {

src/commands/provision.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ use tracing::{info, instrument};
2020
use crate::ansible::AnsibleTemplateRenderer;
2121
use crate::command::CommandError;
2222
use crate::command_wrappers::ansible::AnsibleClient;
23+
#[allow(unused_imports)]
24+
use crate::command_wrappers::lxd::InstanceName;
2325
use crate::command_wrappers::opentofu::client::{InstanceInfo, OpenTofuError};
2426
use crate::command_wrappers::ssh::{credentials::SshCredentials, SshError};
2527
use crate::steps::{
@@ -196,7 +198,7 @@ mod tests {
196198
template_manager.clone(),
197199
temp_dir.path(),
198200
ssh_credentials.clone(),
199-
"torrust-vm".to_string(), // TODO: Make this configurable in Phase 3
201+
InstanceName::new("torrust-vm".to_string()).expect("Valid hardcoded instance name"), // TODO: Make this configurable in Phase 3
200202
));
201203

202204
let ansible_renderer = Arc::new(AnsibleTemplateRenderer::new(

src/container.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use std::sync::Arc;
1414

1515
use crate::ansible::AnsibleTemplateRenderer;
1616
use crate::command_wrappers::ansible::AnsibleClient;
17-
use crate::command_wrappers::lxd::LxdClient;
17+
use crate::command_wrappers::lxd::{InstanceName, LxdClient};
1818
use crate::command_wrappers::opentofu::OpenTofuClient;
1919
use crate::config::Config;
2020
use crate::template::TemplateManager;
@@ -35,6 +35,11 @@ pub struct Services {
3535

3636
impl Services {
3737
/// Create a new services container using the provided configuration
38+
///
39+
/// # Panics
40+
///
41+
/// Panics if the hardcoded "torrust-vm" instance name is invalid (should never happen
42+
/// as it's a known valid name). This will be made configurable in Phase 3.
3843
#[must_use]
3944
pub fn new(config: &Config) -> Self {
4045
// Create template manager
@@ -56,7 +61,7 @@ impl Services {
5661
template_manager.clone(),
5762
config.build_dir.clone(),
5863
config.ssh_credentials.clone(),
59-
"torrust-vm".to_string(), // TODO: Make this configurable in Phase 3
64+
InstanceName::new("torrust-vm".to_string()).expect("Valid hardcoded instance name"), // TODO: Make this configurable in Phase 3
6065
);
6166

6267
// Create configuration template renderer

src/e2e/tasks/preflight_cleanup.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
use std::fmt;
77
use tracing::{info, warn};
88

9-
use crate::command_wrappers::lxd::client::LxdClient;
9+
#[allow(unused_imports)]
10+
use crate::command_wrappers::lxd::{client::LxdClient, InstanceName};
1011
use crate::command_wrappers::opentofu::{self, EmergencyDestroyError};
1112
use crate::e2e::environment::TestEnvironment;
1213

@@ -275,7 +276,10 @@ fn cleanup_lxd_resources(_env: &TestEnvironment) {
275276
let lxd_client = LxdClient::new();
276277

277278
// Clean up test instance if it exists
278-
match lxd_client.delete_instance("torrust-vm", true) {
279+
match lxd_client.delete_instance(
280+
&InstanceName::new("torrust-vm".to_string()).expect("Valid hardcoded instance name"),
281+
true,
282+
) {
279283
Ok(()) => {
280284
info!(
281285
operation = "lxd_resources_cleanup",

src/template/wrappers/tofu/lxd/variables/context.rs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,20 @@
1414
//!
1515
//! ```rust
1616
//! use torrust_tracker_deploy::template::wrappers::tofu::lxd::variables::VariablesContext;
17+
//! use torrust_tracker_deploy::command_wrappers::lxd::instance::InstanceName;
1718
//!
1819
//! let context = VariablesContext::builder()
19-
//! .with_instance_name("my-test-vm".to_string())
20+
//! .with_instance_name(InstanceName::new("my-test-vm".to_string()).unwrap())
2021
//! .build()
2122
//! .unwrap();
2223
//! ```
2324
2425
use serde::Serialize;
2526
use thiserror::Error;
2627

28+
#[allow(unused_imports)]
29+
use crate::command_wrappers::lxd::instance::InstanceName;
30+
2731
/// Errors that can occur when building the variables context
2832
#[derive(Error, Debug)]
2933
pub enum VariablesContextError {
@@ -39,7 +43,7 @@ pub enum VariablesContextError {
3943
#[derive(Debug, Clone, Serialize)]
4044
pub struct VariablesContext {
4145
/// The name of the VM/container instance to be created
42-
pub instance_name: String,
46+
pub instance_name: InstanceName,
4347
}
4448

4549
/// Builder for creating `VariablesContext` instances
@@ -48,7 +52,7 @@ pub struct VariablesContext {
4852
/// to ensure all required fields are provided.
4953
#[derive(Debug, Default)]
5054
pub struct VariablesContextBuilder {
51-
instance_name: Option<String>,
55+
instance_name: Option<InstanceName>,
5256
}
5357

5458
impl VariablesContextBuilder {
@@ -64,7 +68,7 @@ impl VariablesContextBuilder {
6468
///
6569
/// * `instance_name` - The name to assign to the created instance
6670
#[must_use]
67-
pub fn with_instance_name(mut self, instance_name: String) -> Self {
71+
pub fn with_instance_name(mut self, instance_name: InstanceName) -> Self {
6872
self.instance_name = Some(instance_name);
6973
self
7074
}
@@ -103,17 +107,17 @@ mod tests {
103107
#[test]
104108
fn it_should_create_variables_context_with_instance_name() {
105109
let context = VariablesContext::builder()
106-
.with_instance_name("test-vm".to_string())
110+
.with_instance_name(InstanceName::new("test-vm".to_string()).unwrap())
107111
.build()
108112
.unwrap();
109113

110-
assert_eq!(context.instance_name, "test-vm");
114+
assert_eq!(context.instance_name.as_str(), "test-vm");
111115
}
112116

113117
#[test]
114118
fn it_should_serialize_to_json() {
115119
let context = VariablesContext::builder()
116-
.with_instance_name("test-vm".to_string())
120+
.with_instance_name(InstanceName::new("test-vm".to_string()).unwrap())
117121
.build()
118122
.unwrap();
119123

@@ -125,12 +129,12 @@ mod tests {
125129
#[test]
126130
fn it_should_build_context_with_builder_pattern() {
127131
let result = VariablesContext::builder()
128-
.with_instance_name("my-instance".to_string())
132+
.with_instance_name(InstanceName::new("my-instance".to_string()).unwrap())
129133
.build();
130134

131135
assert!(result.is_ok());
132136
let context = result.unwrap();
133-
assert_eq!(context.instance_name, "my-instance");
137+
assert_eq!(context.instance_name.as_str(), "my-instance");
134138
}
135139

136140
#[test]

src/template/wrappers/tofu/lxd/variables/mod.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ impl VariablesTemplate {
8080
/// Get the instance name value
8181
#[must_use]
8282
pub fn instance_name(&self) -> &str {
83-
&self.context.instance_name
83+
self.context.instance_name.as_str()
8484
}
8585

8686
/// Render the template to a file at the specified output path
@@ -109,11 +109,12 @@ impl VariablesTemplate {
109109
#[cfg(test)]
110110
mod tests {
111111
use super::*;
112+
use crate::command_wrappers::lxd::instance::InstanceName;
112113
use tempfile::NamedTempFile;
113114

114115
fn create_test_context() -> VariablesContext {
115116
VariablesContext::builder()
116-
.with_instance_name("test-instance".to_string())
117+
.with_instance_name(InstanceName::new("test-instance".to_string()).unwrap())
117118
.build()
118119
.unwrap()
119120
}
@@ -200,7 +201,10 @@ image = "ubuntu:24.04""#;
200201
let context = create_test_context();
201202
let variables_template = VariablesTemplate::new(&template_file, context).unwrap();
202203

203-
assert_eq!(variables_template.context().instance_name, "test-instance");
204+
assert_eq!(
205+
variables_template.context().instance_name.as_str(),
206+
"test-instance"
207+
);
204208
}
205209

206210
#[test]
@@ -256,11 +260,14 @@ image = "ubuntu:24.04""#;
256260
let template_file =
257261
File::new("variables.tfvars.tera", "{{ instance_name }}".to_string()).unwrap();
258262
let context = VariablesContext::builder()
259-
.with_instance_name("dynamic-vm".to_string())
263+
.with_instance_name(InstanceName::new("dynamic-vm".to_string()).unwrap())
260264
.build()
261265
.unwrap();
262266

263267
let variables_template = VariablesTemplate::new(&template_file, context).unwrap();
264-
assert_eq!(variables_template.context().instance_name, "dynamic-vm");
268+
assert_eq!(
269+
variables_template.context().instance_name.as_str(),
270+
"dynamic-vm"
271+
);
265272
}
266273
}

0 commit comments

Comments
 (0)