fix(deploy): normalize Docker access errors and harden SSE parsing

- handle malformed plain-text SSE payloads safely
- normalize Docker socket permission errors
- verify authoritative deployment job status
- preserve deployment logs on failure
This commit is contained in:
KodekaTeam committed 2026-07-19 12:54:52 +07:00
1 parent 323596c586
commit 73b4b6255d
8 files changed
+182 -31

No files matched your search

@@ -4,7 +4,12 @@ import { posix as pathPosix } from "path";
import { TextDecoder } from "util";
import { Server } from "@prisma/client";
import { execStrict } from "./commands";
import { execDocker, execDockerStrict } from "./internal/docker";
import {
DOCKER_ACCESS_DENIED_MESSAGE,
execDocker,
execDockerStrict,
parseDockerJson,
} from "./internal/docker";
import { privilegedCommand } from "./internal/privilege";
import { escapeShellArg } from "./internal/shell";
@@ -576,6 +581,15 @@ export function formatDeploymentErrorMessage(error: unknown): string {
const cleaned = trimCommandErrorNoise(raw);
const lower = cleaned.toLowerCase();
if (
lower.includes("permission denied") &&
(lower.includes("docker.sock") ||
lower.includes("docker daemon") ||
lower.includes("docker api"))
) {
return DOCKER_ACCESS_DENIED_MESSAGE;
}
if (
lower.includes("could not read username for 'https://") ||
lower.includes("terminal prompts disabled") ||
@@ -1003,7 +1017,10 @@ export async function dockerInspect(
`docker inspect ${escapeShellArg(containerId)}`,
shortDockerCommandTimeout(DOCKER_INSPECT_TIMEOUT_MS),
);
const parsed = JSON.parse(stdout) as DockerContainerInspect[];
const parsed = parseDockerJson<DockerContainerInspect[]>(
stdout,
"Docker container inspection",
);
return parsed[0] ?? {};
}
@@ -1016,14 +1033,14 @@ export async function dockerStats(
`docker stats --no-stream --format '{{json .}}' ${escapeShellArg(containerId)}`,
shortDockerCommandTimeout(DOCKER_STATS_TIMEOUT_MS),
);
const parsed = JSON.parse(stdout.trim()) as {
const parsed = parseDockerJson<{
CPUPerc?: string;
MemPerc?: string;
MemUsage?: string;
NetIO?: string;
BlockIO?: string;
PIDs?: string;
};
}>(stdout, "Docker container statistics");
return {
cpuPercent: parseFloat((parsed.CPUPerc || "0").replace("%", "")) || 0,
@@ -1848,7 +1865,10 @@ async function inspectImageExposedPorts(server: Server, imageTag: string) {
return [] as string[];
}
const parsed = JSON.parse(normalized) as Record<string, unknown> | null;
const parsed = parseDockerJson<Record<string, unknown> | null>(
normalized,
"Docker image inspection",
);
if (!parsed || typeof parsed !== "object") {
return [] as string[];
}
@@ -4,6 +4,9 @@ import type { SSHExecCommandResponse } from "node-ssh";
import { exec } from "../commands";
import { detectServerPlatform } from "../platform";
export const DOCKER_ACCESS_DENIED_MESSAGE =
"Docker access was denied for the configured SSH user. Grant that user access to the Docker socket (usually by adding it to the docker group), or configure passwordless sudo for Docker commands, then reconnect and retry the deployment.";
function shouldRetryDockerWithSudo(output: string): boolean {
const lower = output.toLowerCase();
return (
@@ -37,6 +40,30 @@ export async function execDocker(
return sudoResult.code === 0 ? sudoResult : directResult;
}
export function parseDockerJson<T>(output: string, operation: string): T {
const normalized = output.trim();
const lower = normalized.toLowerCase();
if (
lower.includes("permission denied") &&
(lower.includes("docker.sock") ||
lower.includes("docker daemon") ||
lower.includes("docker api"))
) {
throw new Error(DOCKER_ACCESS_DENIED_MESSAGE);
}
if (!normalized) {
throw new Error(`${operation} returned an empty response from Docker`);
}
try {
return JSON.parse(normalized) as T;
} catch {
throw new Error(`${operation} returned an invalid response from Docker`);
}
}
export async function execDockerStrict(
server: Server,
command: string,