mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-18 22:36:19 +00:00
fix(stacks): resolve permission denied error on stack deletion (#261)
* fix(stacks): resolve permission denied error when deleting stacks with root-owned files When Docker Compose creates files as root inside a stack directory, the non-root Sencho process cannot remove them. This adds a Docker-based fallback: if fsPromises.rm fails with EACCES/EPERM, Sencho spawns a short-lived Alpine container to clean up the root-owned files. Also enhances docker compose down with --volumes --remove-orphans to let Docker clean up its own resources before filesystem deletion. * docs: clarify that pre-existing root-owned stacks can be deleted * fix(stacks): include Docker stderr in fallback deletion error message Fixes CI lint failure: 'stderr' was assigned but never read in forceDeleteViaDocker(). Now surfaces Docker stderr output in the error message when the fallback cleanup fails.
This commit is contained in:
@@ -378,7 +378,7 @@ export class ComposeService {
|
||||
public async downStack(stackName: string): Promise<void> {
|
||||
const stackPath = path.join(this.baseDir, stackName);
|
||||
try {
|
||||
await this.execute('docker', ['compose', 'down'], stackPath, undefined, false);
|
||||
await this.execute('docker', ['compose', 'down', '--volumes', '--remove-orphans'], stackPath, undefined, false);
|
||||
} catch (error) {
|
||||
console.warn(`[Teardown] Docker down failed or nothing to clean up for ${stackName}`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import path from 'path';
|
||||
import { promises as fsPromises } from 'fs';
|
||||
import { spawn } from 'child_process';
|
||||
import { NodeRegistry } from './NodeRegistry';
|
||||
|
||||
/**
|
||||
@@ -174,14 +175,73 @@ export class FileSystemService {
|
||||
try {
|
||||
await fsPromises.rm(stackDir, { recursive: true, force: true });
|
||||
console.log('Stack deleted successfully:', 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}`);
|
||||
} catch (error: unknown) {
|
||||
const fsError = error as NodeJS.ErrnoException;
|
||||
if (fsError.code === 'ENOENT') return;
|
||||
|
||||
if (fsError.code === 'EACCES' || fsError.code === 'EPERM') {
|
||||
console.warn(
|
||||
`[FileSystemService] Permission denied deleting ${stackName}, falling back to Docker-based removal`
|
||||
);
|
||||
await this.forceDeleteViaDocker(stackDir);
|
||||
// Docker removes contents but can't remove its own mount point; clean up the empty shell
|
||||
try {
|
||||
await fsPromises.rmdir(stackDir);
|
||||
} catch {
|
||||
console.warn(`[FileSystemService] Could not remove empty directory ${stackDir} — may need manual cleanup`);
|
||||
}
|
||||
console.log('Stack deleted successfully (via Docker fallback):', stackName);
|
||||
} else {
|
||||
console.error('Error deleting stack directory:', fsError.message);
|
||||
throw new Error(`Failed to delete stack directory: ${fsError.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private forceDeleteViaDocker(dirPath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = 30_000;
|
||||
const child = spawn('docker', [
|
||||
'run', '--rm',
|
||||
'-v', `${dirPath}:/cleanup`,
|
||||
'alpine',
|
||||
'rm', '-rf', '/cleanup'
|
||||
], {
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
|
||||
}
|
||||
});
|
||||
|
||||
let stderr = '';
|
||||
child.stderr.on('data', (data: Buffer) => { stderr += data.toString(); });
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
child.kill();
|
||||
reject(new Error(
|
||||
`Docker-based deletion timed out after 30s. You may need to manually remove the directory: ${dirPath}`
|
||||
));
|
||||
}, timeout);
|
||||
|
||||
child.on('close', (code: number | null) => {
|
||||
clearTimeout(timer);
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(
|
||||
`Failed to delete stack directory — Docker cleanup exited with code ${code}${stderr ? ': ' + stderr.trim() : ''}. ` +
|
||||
`You may need to manually remove the directory: ${dirPath}`
|
||||
));
|
||||
});
|
||||
|
||||
child.on('error', (err: Error) => {
|
||||
clearTimeout(timer);
|
||||
reject(new Error(
|
||||
`Failed to delete stack directory — could not run Docker for cleanup: ${err.message}. ` +
|
||||
`You may need to manually remove the directory: ${dirPath}`
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getBaseDir(): string {
|
||||
return this.baseDir;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user