diff --git a/CHANGELOG.md b/CHANGELOG.md index fbd57455..a7e50879 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- **Fixed:** Atomic Rollback failure where non-empty directories caused silent file system errors. +- **Added:** Two-Stage Teardown mechanism to ensure `docker compose down` sweeps up ghost networks before deployment files are deleted. - **Added:** Smart Error Parser with telemetry-ready rule IDs to translate cryptic Docker output. - **Added:** Post-Deploy Health Probe to catch immediate container crashes that slip past Compose. - **Changed:** Rollback engine respects a `canSilentlyRollback` flag to protect user-authored configurations. diff --git a/backend/src/index.ts b/backend/src/index.ts index 770aeae7..1f673376 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1095,7 +1095,19 @@ app.post('/api/templates/deploy', async (req: Request, res: Response) => { const shouldRollback = parsed.rule ? parsed.rule.canSilentlyRollback : true; if (shouldRollback) { - await fileSystemService.deleteStack(stackName); + try { + // Stage 1: Tell Docker to clean up ghost networks/containers + await composeService.downStack(stackName); + } catch (downErr) { + console.error("Rollback Stage 1 (Docker down) failed:", downErr); + } + + try { + // Stage 2: Obliterate the files + await fileSystemService.deleteStack(stackName); + } catch (fsErr) { + console.error("Rollback Stage 2 (File deletion) failed:", fsErr); + } } res.status(500).json({ diff --git a/backend/src/services/ComposeService.ts b/backend/src/services/ComposeService.ts index 80768886..1caf2e32 100644 --- a/backend/src/services/ComposeService.ts +++ b/backend/src/services/ComposeService.ts @@ -1,9 +1,12 @@ -import { spawn } from 'child_process'; +import { spawn, exec } from 'child_process'; +import { promisify } from 'util'; import path from 'path'; import WebSocket from 'ws'; import DockerController from './DockerController'; import { LogFormatter } from './LogFormatter'; +const execAsync = promisify(exec); + export class ComposeService { private baseDir: string; @@ -394,4 +397,14 @@ export class ComposeService { }); }); } + + public async downStack(stackName: string): Promise { + const stackPath = path.join(this.baseDir, stackName); + try { + // Run down to clean up any partially created networks or containers + await execAsync(`docker compose -f compose.yaml down`, { cwd: stackPath }); + } catch (error) { + console.warn(`[Teardown] Docker down failed or nothing to clean up for ${stackName}`); + } + } } diff --git a/backend/src/services/FileSystemService.ts b/backend/src/services/FileSystemService.ts index 9be14689..15dc2cbd 100644 --- a/backend/src/services/FileSystemService.ts +++ b/backend/src/services/FileSystemService.ts @@ -194,15 +194,17 @@ export class FileSystemService { /** * Delete a stack (entire directory and its contents) */ - async deleteStack(stackName: string): Promise { + public async deleteStack(stackName: string): Promise { const stackDir = path.join(this.baseDir, stackName); try { await fs.rm(stackDir, { recursive: true, force: true }); console.log('Stack deleted successfully:', stackName); - } catch (error) { - console.error('Error deleting stack:', error); - throw new Error(`Failed to delete stack: ${stackName}`); + } catch (error: any) { + if (error.code !== 'ENOENT') { + console.error('Error deleting stack directory:', error.message); + throw new Error(`Failed to delete stack directory: ${error.message}`); + } } }