mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-11 11:16:55 +00:00
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:
File diff suppressed because it is too large
Load Diff
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user