Skip to content

Commit 52aab05

Browse files
committed
feat: enhance preflight cleanup with LXD resource management
- Add delete_instance() and delete_profile() methods to LXD client - Implement cleanup_lxd_resources() in preflight cleanup module - Use command wrapper pattern instead of direct std::process::Command - Prevent 'profile already exists' errors in E2E test runs - Ensure complete resource isolation between test runs The cleanup now handles: - Build directory cleanup (existing) - OpenTofu infrastructure cleanup (existing) - LXD instances and profiles cleanup (new) This resolves the issue where lingering LXD resources from interrupted test runs would cause subsequent E2E tests to fail with resource conflict errors.
1 parent 953d139 commit 52aab05

File tree

4 files changed

+303
-154
lines changed

4 files changed

+303
-154
lines changed

docs/issues/build-directory-not-cleaned-between-e2e-runs.md

Lines changed: 0 additions & 122 deletions
This file was deleted.

docs/refactors/instance-name-parameterization.md

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -90,28 +90,6 @@ This refactor aims to eliminate hardcoded "torrust-vm" instance names throughout
9090
- **Status**: Template wrapper ready, needs integration into workflow
9191
- **Validation**: E2E tests should show dynamic instance naming
9292

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`
106-
107-
**Solutions Available**:
108-
109-
1. Clean in `TofuTemplateRenderer.create_build_directory()` (recommended)
110-
2. Clean in E2E preflight cleanup
111-
3. Clean in `RenderOpenTofuTemplatesStep`
112-
113-
**Priority**: High - Must be fixed to continue refactor validation
114-
11593
### Phase 3: Context Integration
11694

11795
#### Step 3: Add instance_name to TofuContext

src/command_wrappers/lxd/client.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,99 @@ impl LxdClient {
206206

207207
LxdJsonParser::parse_instances_json(&output)
208208
}
209+
210+
/// Delete an LXD instance
211+
///
212+
/// # Arguments
213+
///
214+
/// * `instance_name` - Name of the instance to delete
215+
/// * `force` - Whether to force deletion (stop running instances)
216+
///
217+
/// # Returns
218+
/// * `Ok(())` - Instance deleted successfully or didn't exist
219+
/// * `Err(anyhow::Error)` - Error describing what went wrong
220+
///
221+
/// # Errors
222+
///
223+
/// This function will return an error if:
224+
/// * The LXD command fails with an unexpected error
225+
/// * LXD is not installed or accessible
226+
pub fn delete_instance(&self, instance_name: &str, force: bool) -> Result<()> {
227+
info!("Deleting LXD instance: {}", instance_name);
228+
229+
let mut args = vec!["delete", instance_name];
230+
if force {
231+
args.push("--force");
232+
}
233+
234+
let result = self.command_executor.run_command("lxc", &args, None);
235+
236+
match result {
237+
Ok(_) => {
238+
info!("LXD instance '{}' deleted successfully", instance_name);
239+
Ok(())
240+
}
241+
Err(e) => {
242+
let error_msg = e.to_string();
243+
// Instance not found is not an error for cleanup operations
244+
if error_msg.contains("not found") || error_msg.contains("does not exist") {
245+
info!(
246+
"LXD instance '{}' doesn't exist, skipping deletion",
247+
instance_name
248+
);
249+
Ok(())
250+
} else {
251+
Err(anyhow::Error::from(e)
252+
.context(format!("Failed to delete LXD instance '{instance_name}'")))
253+
}
254+
}
255+
}
256+
}
257+
258+
/// Delete an LXD profile
259+
///
260+
/// # Arguments
261+
///
262+
/// * `profile_name` - Name of the profile to delete
263+
///
264+
/// # Returns
265+
/// * `Ok(())` - Profile deleted successfully or didn't exist
266+
/// * `Err(anyhow::Error)` - Error describing what went wrong
267+
///
268+
/// # Errors
269+
///
270+
/// This function will return an error if:
271+
/// * The LXD command fails with an unexpected error
272+
/// * LXD is not installed or accessible
273+
/// * Profile is in use by existing instances
274+
pub fn delete_profile(&self, profile_name: &str) -> Result<()> {
275+
info!("Deleting LXD profile: {}", profile_name);
276+
277+
let args = vec!["profile", "delete", profile_name];
278+
279+
let result = self.command_executor.run_command("lxc", &args, None);
280+
281+
match result {
282+
Ok(_) => {
283+
info!("LXD profile '{}' deleted successfully", profile_name);
284+
Ok(())
285+
}
286+
Err(e) => {
287+
let error_msg = e.to_string();
288+
// Profile not found is not an error for cleanup operations
289+
if error_msg.contains("not found") || error_msg.contains("does not exist") {
290+
info!(
291+
"LXD profile '{}' doesn't exist, skipping deletion",
292+
profile_name
293+
);
294+
Ok(())
295+
} else {
296+
Err(anyhow::Error::from(e)
297+
.context(format!("Failed to delete LXD profile '{profile_name}'")))
298+
}
299+
}
300+
}
301+
}
209302
}
210303

211304
#[cfg(test)]

0 commit comments

Comments
 (0)