fix: harden atomic deployment rollback (#1029)

* fix: harden atomic deployment rollback

* fix: update Docker toolchain to Go 1.26.3

* fix: repair Dockerfile tr argument split across lines

* fix: bump protobufjs to clear npm audit high-severity advisories

* fix: sanitize error objects in console.error to prevent log injection
This commit is contained in:
Anso
2026-05-12 15:58:30 -04:00
committed by GitHub
parent 69b6ac1f3b
commit 74ae2ce0c6
11 changed files with 739 additions and 530 deletions
File diff suppressed because it is too large Load Diff
+14 -7
View File
@@ -51,11 +51,11 @@ const MIME_MAP: Record<string, string> = {
*/
export class FileSystemService {
private baseDir: string;
private nodeId: number;
constructor(nodeId?: number) {
this.baseDir = NodeRegistry.getInstance().getComposeDir(
nodeId ?? NodeRegistry.getInstance().getDefaultNodeId()
);
this.nodeId = nodeId ?? NodeRegistry.getInstance().getDefaultNodeId();
this.baseDir = NodeRegistry.getInstance().getComposeDir(this.nodeId);
}
public static getInstance(nodeId?: number): FileSystemService {
@@ -77,6 +77,13 @@ export class FileSystemService {
return stackDir;
}
private getBackupDir(stackName: string): string {
if (!isValidStackName(stackName)) {
throw Object.assign(new Error('Invalid stack name'), { code: 'INVALID_STACK_NAME' });
}
return path.join(getBackupBaseDir(), String(this.nodeId), stackName);
}
async hasComposeFile(dir: string): Promise<boolean> {
this.assertWithinBase(dir);
const composeFiles = ['compose.yaml', 'compose.yml', 'docker-compose.yaml', 'docker-compose.yml'];
@@ -298,7 +305,7 @@ export class FileSystemService {
/**
* Backup stack files (compose.yaml + .env) into Sencho's data dir.
*
* Backups live at <DATA_DIR>/backups/<stackName>/ (NOT inside the user's
* Backups live at <DATA_DIR>/backups/<nodeId>/<stackName>/ (NOT inside the user's
* compose folder) so the operation always succeeds even when the stack
* folder is owned by another UID (e.g., a container running as root has
* chowned its bind mount). DATA_DIR is the same writable location that
@@ -308,7 +315,7 @@ export class FileSystemService {
const debug = isDebugEnabled();
const t0 = Date.now();
const stackDir = this.resolveStackDir(stackName);
const backupDir = path.join(getBackupBaseDir(), stackName);
const backupDir = this.getBackupDir(stackName);
await fsPromises.mkdir(backupDir, { recursive: true });
// Copy compose file
@@ -347,7 +354,7 @@ export class FileSystemService {
const debug = isDebugEnabled();
const t0 = Date.now();
const stackDir = this.resolveStackDir(stackName);
const backupDir = path.join(getBackupBaseDir(), stackName);
const backupDir = this.getBackupDir(stackName);
const items = await fsPromises.readdir(backupDir);
for (const item of items) {
@@ -358,7 +365,7 @@ export class FileSystemService {
}
async getBackupInfo(stackName: string): Promise<{ exists: boolean; timestamp: number | null }> {
const backupDir = path.join(getBackupBaseDir(), stackName);
const backupDir = this.getBackupDir(stackName);
try {
await fsPromises.access(backupDir);
const tsFile = path.join(backupDir, '.timestamp');
+4
View File
@@ -2,6 +2,7 @@ import { CronExpressionParser } from 'cron-parser';
import { DatabaseService } from './DatabaseService';
import type { ScheduledTask } from './DatabaseService';
import { LicenseService } from './LicenseService';
import { PROXY_TIER_HEADER, PROXY_VARIANT_HEADER } from './license-headers';
import DockerController from './DockerController';
import { ComposeService } from './ComposeService';
import { FileSystemService } from './FileSystemService';
@@ -625,6 +626,7 @@ export class SchedulerService {
}
const baseUrl = proxyTarget.apiUrl.replace(/\/$/, '');
const proxyHeaders = LicenseService.getInstance().getProxyHeaders();
if (isDebugEnabled()) {
console.log(`[SchedulerService] executeUpdateRemote: node=${nodeId} target=${target}`);
}
@@ -634,6 +636,8 @@ export class SchedulerService {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${proxyTarget.apiToken}`,
[PROXY_TIER_HEADER]: proxyHeaders.tier,
[PROXY_VARIANT_HEADER]: proxyHeaders.variant ?? '',
},
body: JSON.stringify({ target }),
signal: AbortSignal.timeout(300_000), // 5 minute timeout for long updates