mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-25 17:57:06 +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:
@@ -100,7 +100,7 @@ vi.mock('../services/LogFormatter', () => ({
|
||||
LogFormatter: { formatLine: (line: string) => line },
|
||||
}));
|
||||
|
||||
import { ComposeService } from '../services/ComposeService';
|
||||
import { ComposeService, getComposeRollbackInfo } from '../services/ComposeService';
|
||||
|
||||
/** Creates an EventEmitter that mimics a child_process spawn result */
|
||||
function createMockProcess() {
|
||||
@@ -235,7 +235,19 @@ describe('ComposeService - deployStack', () => {
|
||||
expect(mockBackupStackFiles).toHaveBeenCalledWith('my-stack');
|
||||
});
|
||||
|
||||
it('throws CONTAINER_CRASHED when exited container has non-zero exit code', async () => {
|
||||
it('aborts atomic deploy before docker side effects when backup fails', async () => {
|
||||
mockBackupStackFiles.mockRejectedValueOnce(new Error('disk full'));
|
||||
|
||||
const svc = ComposeService.getInstance(1);
|
||||
|
||||
await expect(svc.deployStack('my-stack', undefined, true)).rejects.toThrow(
|
||||
'Atomic deployment backup failed',
|
||||
);
|
||||
expect(mockSpawn).not.toHaveBeenCalled();
|
||||
expect(mockGetContainersByStack).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws sanitized CONTAINER_CRASHED when exited container has non-zero exit code', async () => {
|
||||
setupAutoCloseSpawn();
|
||||
mockListContainers.mockResolvedValue([{
|
||||
Id: 'crashed-c1',
|
||||
@@ -243,7 +255,7 @@ describe('ComposeService - deployStack', () => {
|
||||
Labels: { 'com.docker.compose.project': 'my-stack' },
|
||||
}]);
|
||||
mockContainerInspect.mockResolvedValue({ State: { ExitCode: 1 } });
|
||||
mockContainerLogs.mockResolvedValue(Buffer.from('Error: something failed'));
|
||||
mockContainerLogs.mockResolvedValue(Buffer.from('SECRET_TOKEN=leaked'));
|
||||
|
||||
const svc = ComposeService.getInstance(1);
|
||||
// Attach catch handler immediately so rejection is never "unhandled"
|
||||
@@ -253,6 +265,8 @@ describe('ComposeService - deployStack', () => {
|
||||
const error = await result;
|
||||
expect(error).not.toBeNull();
|
||||
expect(error!.message).toContain('CONTAINER_CRASHED');
|
||||
expect(error!.message).not.toContain('SECRET_TOKEN');
|
||||
expect(mockContainerLogs).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rolls back on failure when atomic=true', async () => {
|
||||
@@ -272,9 +286,29 @@ describe('ComposeService - deployStack', () => {
|
||||
const error = await result;
|
||||
expect(error).not.toBeNull();
|
||||
expect(error!.message).toContain('CONTAINER_CRASHED');
|
||||
expect(getComposeRollbackInfo(error)).toEqual({ attempted: true, rolledBack: true });
|
||||
expect(mockRestoreStackFiles).toHaveBeenCalledWith('my-stack');
|
||||
});
|
||||
|
||||
it('reports rollback failure when atomic restore fails', async () => {
|
||||
setupAutoCloseSpawn();
|
||||
mockListContainers.mockResolvedValue([{
|
||||
Id: 'crashed-c1',
|
||||
State: 'exited',
|
||||
Labels: { 'com.docker.compose.project': 'my-stack' },
|
||||
}]);
|
||||
mockContainerInspect.mockResolvedValue({ State: { ExitCode: 1 } });
|
||||
mockRestoreStackFiles.mockRejectedValueOnce(new Error('restore denied'));
|
||||
|
||||
const svc = ComposeService.getInstance(1);
|
||||
const result = svc.deployStack('my-stack', undefined, true).then(() => null, (e: Error) => e);
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
const error = await result;
|
||||
expect(error).not.toBeNull();
|
||||
expect(getComposeRollbackInfo(error)).toEqual({ attempted: true, rolledBack: false });
|
||||
});
|
||||
|
||||
it('does not roll back when atomic=false', async () => {
|
||||
setupAutoCloseSpawn();
|
||||
mockListContainers.mockResolvedValue([{
|
||||
|
||||
Reference in New Issue
Block a user