fix: resolve project env from active container runtime path

This commit is contained in:
KodekaTeam committed 2026-07-11 13:08:00 +07:00
1 parent 8a27446643
commit 248053844a
3 files changed
+158 -53

No files matched your search

+89 -53
View File
@@ -787,6 +787,46 @@ function getInspectWorkingDir(inspect: unknown) {
return null;
}
export function getProjectEnvRuntimeCandidatePaths(
inspect: unknown,
mounts: DockerInspectMount[],
processWorkingDirs: string[] = [],
) {
const candidates = new Set<string>();
const workingDir = getInspectWorkingDir(inspect);
processWorkingDirs.forEach((path) => {
if (isUsableMountPath(path)) {
candidates.add(pathPosix.join(normalizeRemotePath(path), ".env"));
}
});
if (workingDir) {
candidates.add(pathPosix.join(normalizeRemotePath(workingDir), ".env"));
}
mounts.forEach((mount) => {
if (isUsableMountPath(mount.Destination)) {
candidates.add(
pathPosix.join(normalizeRemotePath(mount.Destination!), ".env"),
);
}
});
[
"/app",
"/workspace",
"/var/www/html",
"/bitnami/laravel",
"/laravel/laravel",
"/srv/app",
].forEach((path) => {
candidates.add(pathPosix.join(path, ".env"));
});
return Array.from(candidates).slice(0, 12);
}
function redactSecret(message: string, secret?: string | null) {
if (!secret) return message;
return message.split(secret).join("[REDACTED]");
@@ -1967,8 +2007,9 @@ export async function containerRoutes(app: FastifyInstance) {
const runtimeId = container.dockerId || container.name;
try {
const containerCandidatePaths = new Set<string>();
const checkedContainerPaths: string[] = [];
let inspect: unknown = null;
const processWorkingDirs: string[] = [];
let projectMounts = parseStoredVolumeMounts(container.volumes);
try {
@@ -1977,27 +2018,57 @@ export async function containerRoutes(app: FastifyInstance) {
// Keep stored deployment candidates and common fallback paths.
}
if (inspect) {
projectMounts = [...getInspectMounts(inspect), ...projectMounts];
const workingDir = getInspectWorkingDir(inspect);
if (workingDir) {
containerCandidatePaths.add(pathPosix.join(workingDir, ".env"));
if (container.status === "RUNNING") {
const mainProcessWorkingDir =
await ssh.getContainerMainProcessWorkingDirectory(
container.server,
runtimeId,
);
if (mainProcessWorkingDir) {
processWorkingDirs.push(mainProcessWorkingDir);
}
}
projectMounts.forEach((mount) => {
if (isUsableMountPath(mount.Destination)) {
containerCandidatePaths.add(
pathPosix.join(normalizeRemotePath(mount.Destination!), ".env"),
);
if (inspect) {
projectMounts = [...getInspectMounts(inspect), ...projectMounts];
}
const containerCandidatePaths = getProjectEnvRuntimeCandidatePaths(
inspect,
projectMounts,
processWorkingDirs,
);
if (container.status === "RUNNING") {
for (const candidatePath of containerCandidatePaths) {
checkedContainerPaths.push(candidatePath);
try {
const file = await ssh.readContainerFile(
container.server,
runtimeId,
candidatePath,
);
return reply.send({
success: true,
data: {
found: true,
path: file.path,
content: file.content,
checkedPaths: checkedContainerPaths,
source: "container",
message:
"Project .env file loaded from the active container filesystem.",
},
});
} catch {
// Try the next runtime project path.
}
}
});
}
containerCandidatePaths.add("/app/.env");
containerCandidatePaths.add("/workspace/.env");
containerCandidatePaths.add("/var/www/html/.env");
const mountCandidatePaths = Array.from(containerCandidatePaths)
const mountCandidatePaths = containerCandidatePaths
.map((path) =>
resolveContainerPathToHostMountPath(path, projectMounts),
)
@@ -2020,46 +2091,11 @@ export async function containerRoutes(app: FastifyInstance) {
});
}
const checkedContainerPaths: string[] = [];
if (container.status === "RUNNING") {
for (const candidatePath of containerCandidatePaths) {
checkedContainerPaths.push(candidatePath);
try {
const file = await ssh.readContainerFile(
container.server,
runtimeId,
candidatePath,
);
return reply.send({
success: true,
data: {
found: true,
path: file.path,
content: file.content,
checkedPaths: [
...data.checkedPaths,
...checkedContainerPaths,
],
source: "container",
message:
"Project .env file loaded from container filesystem.",
},
});
} catch {
// Try the next common app path.
}
}
}
return reply.send({
success: true,
data: {
...data,
checkedPaths: [
...data.checkedPaths,
...checkedContainerPaths,
],
checkedPaths: [...data.checkedPaths, ...checkedContainerPaths],
source: "missing",
message:
"No project .env file was found at the checked deployment, mounted project, or container paths.",
@@ -1140,6 +1140,35 @@ export async function listContainerFiles(
};
}
export async function getContainerMainProcessWorkingDirectory(
server: Server,
containerId: string,
): Promise<string | null> {
const script = [
'CWD=$(readlink -f /proc/1/cwd 2>/dev/null || true)',
'if [ -z "$CWD" ] || [ ! -d "$CWD" ]; then exit 0; fi',
'CWD_B64=$(printf "%s" "$CWD" | base64 | tr -d "\\n")',
'printf "__CWD__\t%s\n" "$CWD_B64"',
].join("\n");
try {
const stdout = await execContainerShell(
server,
containerId,
script,
DOCKER_FILE_LIST_TIMEOUT_MS,
);
const line = stdout
.split("\n")
.find((entry) => entry.startsWith("__CWD__\t"));
const encodedPath = line?.split("\t")[1];
return encodedPath ? decodeBase64Value(encodedPath) : null;
} catch {
return null;
}
}
export async function readContainerFile(
server: Server,
containerId: string,
@@ -3,6 +3,7 @@ import test from "node:test";
import {
clearContainerMetricsCacheForTest,
getProjectEnvRuntimeCandidatePaths,
invalidateContainerMetricsCache,
parseStoredVolumeMounts,
readContainerMetricsWithCache,
@@ -174,3 +175,42 @@ test("project env path resolver can use stored container volume bindings", () =>
);
assert.equal(mounts[0]?.Destination, "/bitnami/laravel");
});
test("project env runtime candidates prefer inspect paths before common fallbacks", () => {
const mounts = parseStoredVolumeMounts([
"/srv/project:/runtime/project",
]);
const paths = getProjectEnvRuntimeCandidatePaths(
{
Config: {
WorkingDir: "/runtime/project",
},
},
mounts,
);
assert.deepEqual(paths.slice(0, 4), [
"/runtime/project/.env",
"/app/.env",
"/workspace/.env",
"/var/www/html/.env",
]);
});
test("project env runtime candidates prefer the main process working directory", () => {
const paths = getProjectEnvRuntimeCandidatePaths(
{
Config: {
WorkingDir: "/image/default",
},
},
[],
["/app"],
);
assert.deepEqual(paths.slice(0, 3), [
"/app/.env",
"/image/default/.env",
"/workspace/.env",
]);
});