mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-19 06:46:23 +00:00
feat(scheduler): consistent action targeting in Scheduled Operations (#1431)
Give every scheduled action an explicit, predictable target model (Action then Node then Stack then Options then Schedule): - System Prune now exposes a Node picker and requires a node, so it can no longer run silently on the default node. - Vulnerability Scan and System Prune list local nodes only; both run on the hub-local Docker daemon and reject remote nodes on the backend. - Restart Stack service discovery loads services from the selected node via fetchForNode instead of the active or local node. - Fleet Snapshot shows a read-only "Scope: Entire fleet" summary. Backend gains a shared local-node guard and prune node validation on create and update, plus an executor-level remote-node guard, so the frontend and backend validation now agree for every action.
This commit is contained in:
@@ -986,22 +986,39 @@ export class FileSystemService {
|
||||
// credit the barrier, which it does not through the helper.
|
||||
const baseResolved = path.resolve(this.baseDir);
|
||||
const checksums: Record<string, string> = {};
|
||||
const writeManagedBackupFile = async (file: string, src: string): Promise<void> => {
|
||||
let buf: Buffer;
|
||||
try {
|
||||
buf = await fsPromises.readFile(src);
|
||||
} catch (e: unknown) {
|
||||
const code = (e as NodeJS.ErrnoException)?.code;
|
||||
if (code !== 'ENOENT') {
|
||||
console.warn(`[FileSystemService] Could not read ${file} for backup:`, (e as Error).message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const dest = path.join(backupDir, file);
|
||||
try {
|
||||
await fsPromises.writeFile(dest, buf);
|
||||
} catch (e: unknown) {
|
||||
try {
|
||||
await fsPromises.unlink(dest);
|
||||
} catch {
|
||||
// Best-effort cleanup only. The write failure below is the actionable error.
|
||||
}
|
||||
throw new Error(`Could not write backup ${file}: ${(e as Error).message}`, { cause: e });
|
||||
}
|
||||
checksums[file] = sha256HexBuffer(buf);
|
||||
};
|
||||
|
||||
const composeFiles = ['compose.yaml', 'compose.yml', 'docker-compose.yaml', 'docker-compose.yml'];
|
||||
for (const file of composeFiles) {
|
||||
const src = path.resolve(baseResolved, path.join(stackDir, file));
|
||||
if (!src.startsWith(baseResolved + path.sep)) {
|
||||
throw Object.assign(new Error('Path escapes compose directory'), { code: 'INVALID_PATH' });
|
||||
}
|
||||
try {
|
||||
const buf = await fsPromises.readFile(src);
|
||||
await fsPromises.writeFile(path.join(backupDir, file), buf);
|
||||
checksums[file] = sha256HexBuffer(buf);
|
||||
} catch (e: unknown) {
|
||||
const code = (e as NodeJS.ErrnoException)?.code;
|
||||
if (code !== 'ENOENT') {
|
||||
console.warn(`[FileSystemService] Could not back up ${file}:`, (e as Error).message);
|
||||
}
|
||||
}
|
||||
await writeManagedBackupFile(file, src);
|
||||
}
|
||||
|
||||
// Copy .env if it exists (same inline containment barrier as above).
|
||||
@@ -1009,16 +1026,7 @@ export class FileSystemService {
|
||||
if (!envSrc.startsWith(baseResolved + path.sep)) {
|
||||
throw Object.assign(new Error('Path escapes compose directory'), { code: 'INVALID_PATH' });
|
||||
}
|
||||
try {
|
||||
const buf = await fsPromises.readFile(envSrc);
|
||||
await fsPromises.writeFile(path.join(backupDir, '.env'), buf);
|
||||
checksums['.env'] = sha256HexBuffer(buf);
|
||||
} catch (e: unknown) {
|
||||
const code = (e as NodeJS.ErrnoException)?.code;
|
||||
if (code !== 'ENOENT') {
|
||||
console.warn('[FileSystemService] Could not back up .env:', (e as Error).message);
|
||||
}
|
||||
}
|
||||
await writeManagedBackupFile('.env', envSrc);
|
||||
|
||||
// Write the integrity manifest before the timestamp marker, so a crash
|
||||
// between the two leaves the checksums present (a backup that restore can
|
||||
|
||||
Reference in New Issue
Block a user