Skip to content

Commit 953d139

Browse files
committed
feat: complete Phase 1-2 of instance name parameterization
- Add .tfvars extension support to template engine - Create variables template wrapper infrastructure - Add variables.tfvars.tera template with {{instance_name}} placeholder - Export variables module from LXD template wrappers - Update refactor plan with progress status This completes the foundational template infrastructure needed for dynamic instance name parameterization in OpenTofu templates.
1 parent f219aa0 commit 953d139

File tree

7 files changed

+516
-18
lines changed

7 files changed

+516
-18
lines changed

docs/refactors/instance-name-parameterization.md

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,35 @@
22

33
## Overview
44

5-
This refactor aims t## 🔄 Design Updates
5+
This refactor aims to eliminate hardcoded "torrust-vm" instance names throughout the codebase and replace them with parameterized values. The goal is to enable dynamic instance naming for different environments and support running multiple instances simultaneously.
6+
7+
## 📊 Progress Status
8+
9+
### ✅ Completed Phases
10+
11+
- **Phase 1**: Foundation - OpenTofu Variables Infrastructure
12+
13+
- ✅ Step 1a: Created OpenTofu variables template (`variables.tfvars`)
14+
- ✅ Step 1b: Updated OpenTofu client for variables file support
15+
16+
- **Phase 2**: Template Parameterization
17+
- ✅ Step 2a: Converted variables.tfvars to Tera template with `{{instance_name}}` placeholder
18+
- ✅ Step 2b: Created template wrapper infrastructure (`VariablesTemplate`, `VariablesContext`)
19+
20+
### 🔄 Current Phase
21+
22+
- **Phase 2c**: Integrate Variables Template Rendering (In Progress)
23+
- 🔄 Add `VariablesTemplate` rendering to `RenderOpenTofuTemplatesStep`
24+
- 🔄 Pass `instance_name` context from provision workflow
25+
- 🔄 Replace static `variables.tfvars` with dynamic rendering
26+
27+
### 📋 Remaining Phases
28+
29+
- **Phase 3**: Context Integration - Add instance_name to workflow context
30+
- **Phase 4**: E2E Integration - Update E2E tests infrastructure context
31+
- **Phase 5**: Complete Migration - Update remaining hardcoded references
32+
33+
## 🔄 Design Updates
634

735
### Variable Naming Convention
836

@@ -18,33 +46,71 @@ This refactor aims t## 🔄 Design Updates
1846

1947
#### Step 1a: Create OpenTofu variables template ✅
2048

21-
- Create `templates/tofu/lxd/variables.tfvars` template file to define `instance_name` variable
22-
- Update `TofuTemplateRenderer` to include this file in static template copying
23-
- Keep `image` variable static (not templated)
49+
- Create `templates/tofu/lxd/variables.tfvars` template file to define `instance_name` variable
50+
- Update `TofuTemplateRenderer` to include this file in static template copying
51+
- Keep `image` variable static (not templated)
2452
- **Status**: Static variables file created with hardcoded "torrust-vm" value
25-
- **Validation**: Unit tests + linters + e2e tests must pass
53+
- **Validation**: Unit tests + linters + e2e tests passed
2654

27-
#### Step 1b: Update OpenTofu client for variables file
55+
#### Step 1b: Update OpenTofu client for variables file
2856

29-
- Modify OpenTofu client to pass `-var-file` parameter to `tofu` commands
30-
- Update unit tests for `TofuClient`
31-
- **Validation**: All OpenTofu commands work with variables file
57+
- ✅ Modify OpenTofu client to pass `-var-file` parameter to `tofu` commands
58+
- ✅ Update unit tests for `TofuClient`
59+
- **Status**: OpenTofu client accepts `extra_args` parameter for `-var-file=variables.tfvars`
60+
- **Validation**: ✅ All OpenTofu commands work with variables file
3261

33-
### Phase 2: Template Parameterization
62+
### Phase 2: Template Parameterization
3463

3564
**Goal**: Convert static variables to dynamic Tera templates
3665

37-
#### Step 2a: Convert variables.tfvars to Tera template
66+
#### Step 2a: Convert variables.tfvars to Tera template ✅
67+
68+
- ✅ Transform static `variables.tfvars` into `variables.tfvars.tera` template with `{{instance_name}}` placeholder
69+
- ✅ Update `TofuTemplateRenderer` to render it with context
70+
- **Status**: Template created with Tera placeholder for dynamic rendering
71+
- **Validation**: ✅ Rendered file contains correct instance name value
72+
73+
#### Step 2b: Create template wrapper for variables rendering ✅
74+
75+
- ✅ Create `VariablesTemplate` and `VariablesContext` following cloud-init pattern
76+
- ✅ Add `.tfvars` extension support to template engine
77+
- ✅ Implement comprehensive template validation and rendering
78+
- **Status**: Template wrapper infrastructure complete with 14 new unit tests
79+
- **Validation**: ✅ Template rendering works, all tests pass
80+
81+
### Phase 2c: Integrate Variables Template Rendering (In Progress) 🔄
82+
83+
**Goal**: Add variables template rendering to infrastructure workflow
84+
85+
#### Step 2c: Add variables rendering to workflow
86+
87+
- 🔄 Add `VariablesTemplate` rendering to `RenderOpenTofuTemplatesStep`
88+
- 🔄 Pass `instance_name` context from provision workflow
89+
- 🔄 Replace static `variables.tfvars` with dynamic rendering
90+
- **Status**: Template wrapper ready, needs integration into workflow
91+
- **Validation**: E2E tests should show dynamic instance naming
92+
93+
## 🐛 Known Issues
94+
95+
### Build Directory Cleanup Issue
96+
97+
**Problem**: E2E tests do not clean the `build/` directory between runs, causing stale template files to persist.
98+
99+
**Impact**:
100+
101+
- Template changes not reflected in E2E test runs (e.g., `instance_name` vs `container_name`)
102+
- Inconsistent behavior between fresh and cached environments
103+
- Blocks validation of template parameterization changes
104+
105+
**Status**: Issue documented in `docs/issues/build-directory-not-cleaned-between-e2e-runs.md`
38106

39-
- Transform static `variables.tfvars` into `variables.tfvars.tera` template with `{{instance_name}}` placeholder
40-
- Update `TofuTemplateRenderer` to render it with context
41-
- **Validation**: Rendered file contains correct instance name value
107+
**Solutions Available**:
42108

43-
#### Step 2b: Update main.tf to use variables
109+
1. Clean in `TofuTemplateRenderer.create_build_directory()` (recommended)
110+
2. Clean in E2E preflight cleanup
111+
3. Clean in `RenderOpenTofuTemplatesStep`
44112

45-
- Modify `templates/tofu/lxd/main.tf.tera` to use `var.instance_name` instead of hardcoded "torrust-vm"
46-
- Ensure proper OpenTofu variable reference syntax
47-
- **Validation**: Infrastructure deploys with custom instance names
113+
**Priority**: High - Must be fixed to continue refactor validation
48114

49115
### Phase 3: Context Integration
50116

project-words.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ nslookup
2828
nullglob
2929
oneline
3030
pacman
31+
parameterizing
3132
pathbuf
3233
pipefail
3334
prereq

src/template/file.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ pub enum Format {
3939
Yml,
4040
Toml,
4141
Tf,
42+
Tfvars,
4243
}
4344

4445
#[derive(Debug, Clone, PartialEq)]
@@ -48,6 +49,7 @@ pub enum Extension {
4849
Yml,
4950
Toml,
5051
Tf,
52+
Tfvars,
5153
}
5254

5355
#[derive(thiserror::Error, Debug, Clone, PartialEq)]
@@ -98,6 +100,7 @@ impl TryFrom<&str> for Extension {
98100
"yml" => Ok(Extension::Yml),
99101
"toml" => Ok(Extension::Toml),
100102
"tf" => Ok(Extension::Tf),
103+
"tfvars" => Ok(Extension::Tfvars),
101104
_ => Err(extension.to_string()),
102105
}
103106
}
@@ -111,6 +114,7 @@ impl Display for Extension {
111114
Extension::Yml => write!(f, "yml"),
112115
Extension::Toml => write!(f, "toml"),
113116
Extension::Tf => write!(f, "tf"),
117+
Extension::Tfvars => write!(f, "tfvars"),
114118
}
115119
}
116120
}
@@ -237,6 +241,7 @@ impl File {
237241
Extension::Yml | Extension::Yaml => Format::Yml,
238242
Extension::Toml => Format::Toml,
239243
Extension::Tf => Format::Tf,
244+
Extension::Tfvars => Format::Tfvars,
240245
Extension::Tera => {
241246
return Err(Error::InvalidInnerExtension {
242247
path: path.to_string(),
@@ -262,6 +267,7 @@ impl File {
262267
Extension::Yml | Extension::Yaml => Format::Yml,
263268
Extension::Toml => Format::Toml,
264269
Extension::Tf => Format::Tf,
270+
Extension::Tfvars => Format::Tfvars,
265271
Extension::Tera => {
266272
// Single .tera extension without inner extension - not allowed
267273
return Err(Error::MissingInnerExtension {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@
33
//! Contains template wrappers for LXD-specific configuration files.
44
//!
55
//! - `cloud_init` - templates/tofu/lxd/cloud-init.yml.tera (with runtime variables: `ssh_public_key`)
6+
//! - `variables` - templates/tofu/lxd/variables.tfvars.tera (with runtime variables: `instance_name`)
67
78
pub mod cloud_init;
9+
pub mod variables;
810

911
pub use cloud_init::{
1012
CloudInitContext, CloudInitContextBuilder, CloudInitContextError, CloudInitTemplate,
1113
};
14+
15+
pub use variables::{
16+
VariablesContext, VariablesContextBuilder, VariablesContextError, VariablesTemplate,
17+
};
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
//! # `OpenTofu` Variables Context
2+
//!
3+
//! Provides context structures for `OpenTofu` variables template rendering.
4+
//!
5+
//! This module contains the context object that holds runtime values for variable template rendering,
6+
//! specifically for the `variables.tfvars.tera` template used in LXD infrastructure provisioning.
7+
//!
8+
//! ## Context Structure
9+
//!
10+
//! The `VariablesContext` holds:
11+
//! - `instance_name` - The dynamic name for the VM/container instance
12+
//!
13+
//! ## Example Usage
14+
//!
15+
//! ```rust
16+
//! use torrust_tracker_deploy::template::wrappers::tofu::lxd::variables::VariablesContext;
17+
//!
18+
//! let context = VariablesContext::builder()
19+
//! .with_instance_name("my-test-vm".to_string())
20+
//! .build()
21+
//! .unwrap();
22+
//! ```
23+
24+
use serde::Serialize;
25+
use thiserror::Error;
26+
27+
/// Errors that can occur when building the variables context
28+
#[derive(Error, Debug)]
29+
pub enum VariablesContextError {
30+
/// Instance name is required but was not provided
31+
#[error("Instance name is required but was not provided")]
32+
MissingInstanceName,
33+
}
34+
35+
/// Context for `OpenTofu` variables template rendering
36+
///
37+
/// Contains all runtime values needed to render `variables.tfvars.tera`
38+
/// with dynamic instance naming and other configurable parameters.
39+
#[derive(Debug, Clone, Serialize)]
40+
pub struct VariablesContext {
41+
/// The name of the VM/container instance to be created
42+
pub instance_name: String,
43+
}
44+
45+
/// Builder for creating `VariablesContext` instances
46+
///
47+
/// Provides a fluent interface for constructing the context with validation
48+
/// to ensure all required fields are provided.
49+
#[derive(Debug, Default)]
50+
pub struct VariablesContextBuilder {
51+
instance_name: Option<String>,
52+
}
53+
54+
impl VariablesContextBuilder {
55+
/// Creates a new builder instance
56+
#[must_use]
57+
pub fn new() -> Self {
58+
Self::default()
59+
}
60+
61+
/// Sets the instance name for the VM/container
62+
///
63+
/// # Arguments
64+
///
65+
/// * `instance_name` - The name to assign to the created instance
66+
#[must_use]
67+
pub fn with_instance_name(mut self, instance_name: String) -> Self {
68+
self.instance_name = Some(instance_name);
69+
self
70+
}
71+
72+
/// Builds the `VariablesContext` with validation
73+
///
74+
/// # Returns
75+
///
76+
/// * `Ok(VariablesContext)` if all required fields are present
77+
/// * `Err(VariablesContextError)` if validation fails
78+
///
79+
/// # Errors
80+
///
81+
/// Returns `MissingInstanceName` if instance name was not provided
82+
pub fn build(self) -> Result<VariablesContext, VariablesContextError> {
83+
let instance_name = self
84+
.instance_name
85+
.ok_or(VariablesContextError::MissingInstanceName)?;
86+
87+
Ok(VariablesContext { instance_name })
88+
}
89+
}
90+
91+
impl VariablesContext {
92+
/// Creates a new builder for constructing `VariablesContext`
93+
#[must_use]
94+
pub fn builder() -> VariablesContextBuilder {
95+
VariablesContextBuilder::new()
96+
}
97+
}
98+
99+
#[cfg(test)]
100+
mod tests {
101+
use super::*;
102+
103+
#[test]
104+
fn it_should_create_variables_context_with_instance_name() {
105+
let context = VariablesContext::builder()
106+
.with_instance_name("test-vm".to_string())
107+
.build()
108+
.unwrap();
109+
110+
assert_eq!(context.instance_name, "test-vm");
111+
}
112+
113+
#[test]
114+
fn it_should_serialize_to_json() {
115+
let context = VariablesContext::builder()
116+
.with_instance_name("test-vm".to_string())
117+
.build()
118+
.unwrap();
119+
120+
let json = serde_json::to_string(&context).unwrap();
121+
assert!(json.contains("test-vm"));
122+
assert!(json.contains("instance_name"));
123+
}
124+
125+
#[test]
126+
fn it_should_build_context_with_builder_pattern() {
127+
let result = VariablesContext::builder()
128+
.with_instance_name("my-instance".to_string())
129+
.build();
130+
131+
assert!(result.is_ok());
132+
let context = result.unwrap();
133+
assert_eq!(context.instance_name, "my-instance");
134+
}
135+
136+
#[test]
137+
fn it_should_fail_when_instance_name_is_missing() {
138+
let result = VariablesContext::builder().build();
139+
140+
assert!(matches!(
141+
result.unwrap_err(),
142+
VariablesContextError::MissingInstanceName
143+
));
144+
}
145+
}

0 commit comments

Comments
 (0)