Skip to content

Commit b7e5bc3

Browse files
committed
fix(cli): map function paths to container mounts
1 parent 20e7e8c commit b7e5bc3

3 files changed

Lines changed: 116 additions & 7 deletions

File tree

apps/cli/src/legacy/commands/start/services/edge-runtime.service.integration.test.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1+
import {
2+
existsSync,
3+
mkdirSync,
4+
readFileSync,
5+
realpathSync,
6+
symlinkSync,
7+
writeFileSync,
8+
} from "node:fs";
29
import { join } from "node:path";
310

411
import { describe, expect, it } from "@effect/vitest";
@@ -289,6 +296,55 @@ describe("legacyStartEdgeRuntimeContainer", () => {
289296
}),
290297
);
291298

299+
it.effect("maps canonical function paths to the mounted root target", () =>
300+
Effect.gen(function* () {
301+
const slug = "hello";
302+
const canonicalWorkdir = join(tempWorkdir.current, "canonical");
303+
const linkedWorkdir = join(tempWorkdir.current, "linked");
304+
const canonicalFunctionsDir = join(canonicalWorkdir, "supabase", "functions");
305+
const mountedFunctionsDir = join(linkedWorkdir, "supabase", "functions");
306+
mkdirSync(join(canonicalFunctionsDir, slug), { recursive: true });
307+
symlinkSync(canonicalWorkdir, linkedWorkdir, "dir");
308+
const canonicalEntrypoint = join(
309+
realpathSync(canonicalWorkdir),
310+
"supabase",
311+
"functions",
312+
slug,
313+
"index.ts",
314+
);
315+
writeFileSync(canonicalEntrypoint, "Deno.serve(() => new Response('ok'));");
316+
317+
const fnConfig = {
318+
enabled: true,
319+
verify_jwt: true,
320+
import_map: "",
321+
entrypoint: canonicalEntrypoint,
322+
static_files: [],
323+
env: {},
324+
};
325+
const mock = mockDockerSpawner();
326+
const out = mockOutput();
327+
328+
yield* legacyStartEdgeRuntimeContainer({
329+
...baseInput(linkedWorkdir),
330+
configDeclaredFunctions: { [slug]: fnConfig },
331+
configFunctions: { [slug]: fnConfig },
332+
rawConfigFunctions: { [slug]: fnConfig },
333+
}).pipe(
334+
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, mock.spawner),
335+
Effect.provide(out.layer),
336+
);
337+
338+
const entries = envEntries(mock.runCall!);
339+
const configPrefix = "SUPABASE_INTERNAL_FUNCTIONS_CONFIG=";
340+
const configEntry = entries.find((entry) => entry.startsWith(configPrefix));
341+
expect(configEntry).toBeDefined();
342+
expect(JSON.parse(configEntry!.slice(configPrefix.length))).toMatchObject({
343+
hello: { entrypointPath: toDockerPath(join(mountedFunctionsDir, slug, "index.ts")) },
344+
});
345+
}),
346+
);
347+
292348
it.effect("omits --workdir when no bind mounts the project root into the container (#6035)", () =>
293349
Effect.gen(function* () {
294350
const mock = mockDockerSpawner();

apps/cli/src/legacy/commands/start/start.slim-images.e2e.test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,31 @@ async function containerHealthStatus(name: string): Promise<string> {
105105
return stdout.trim();
106106
}
107107

108+
async function edgeRuntimeFailureDiagnostics(name: string): Promise<string> {
109+
let mounts = "<unavailable>";
110+
try {
111+
const { stdout } = await execFileAsync("docker", [
112+
"inspect",
113+
name,
114+
"--format",
115+
"{{json .Mounts}}",
116+
]);
117+
mounts = stdout.trim() || "[]";
118+
} catch (error) {
119+
mounts = `<unavailable: ${error instanceof Error ? error.message : String(error)}>`;
120+
}
121+
122+
let logs = "<unavailable>";
123+
try {
124+
const { stdout, stderr } = await execFileAsync("docker", ["logs", name]);
125+
logs = `${stdout}${stderr}`.trim() || "<empty>";
126+
} catch (error) {
127+
logs = `<unavailable: ${error instanceof Error ? error.message : String(error)}>`;
128+
}
129+
130+
return `edge runtime Mounts: ${mounts}\nedge runtime logs:\n${logs}`;
131+
}
132+
108133
async function runWgetInImage(
109134
image: string,
110135
args: ReadonlyArray<string>,
@@ -258,7 +283,11 @@ describe("supabase start slim images (e2e)", () => {
258283
body: JSON.stringify({ name: "Functions" }),
259284
});
260285
const body = await invoked.text();
261-
expect(invoked.ok, body).toBe(true);
286+
if (!invoked.ok) {
287+
throw new Error(
288+
`Functions request failed (${invoked.status}): ${body}\n${await edgeRuntimeFailureDiagnostics(edgeRuntimeContainer)}`,
289+
);
290+
}
262291
expect(JSON.parse(body)).toEqual({ message: "Hello Functions!" });
263292
},
264293
);

apps/cli/src/shared/functions/serve.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import {
44
type ResolvedFunctionConfig as ManifestFunctionConfig,
55
} from "@supabase/config/effect";
66
import { edgeRuntimeNofileUlimit } from "../stack-constants.ts";
7-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
8-
import { isAbsolute, join, resolve } from "node:path";
7+
import { mkdir, readFile, realpath, rm, writeFile } from "node:fs/promises";
8+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
99
import { Effect, Option, Redacted, Stream } from "effect";
1010
import { parseDotEnv } from "../../legacy/shared/legacy-dotenv.ts";
1111
import { Output } from "../output/output.service.ts";
@@ -318,8 +318,21 @@ const parseFunctionEnvFile = Effect.fnUntraced(function* (pathname: string) {
318318
function toFunctionContainerConfig(
319319
config: ResolvedDeployFunctionConfig,
320320
envFile: Readonly<Record<string, string>>,
321+
pathMappings: ReadonlyArray<{ readonly hostRoot: string; readonly containerRoot: string }>,
321322
): ServeFunctionContainerConfig {
322-
const toContainerPath = (pathname: string) => toDockerPath(resolve(pathname));
323+
const toContainerPath = (pathname: string) => {
324+
const resolvedPath = resolve(pathname);
325+
for (const mapping of pathMappings) {
326+
const relativePath = relative(mapping.hostRoot, resolvedPath);
327+
if (
328+
relativePath === "" ||
329+
(!isAbsolute(relativePath) && relativePath !== ".." && !relativePath.startsWith(`..${sep}`))
330+
) {
331+
return join(mapping.containerRoot, relativePath).replaceAll("\\", "/");
332+
}
333+
}
334+
return toDockerPath(resolvedPath);
335+
};
323336

324337
return {
325338
// The Go serve path defaults verifyJWT to true when verify_jwt is not set in
@@ -649,6 +662,17 @@ export const startEdgeRuntimeContainer = Effect.fn("functions.startEdgeRuntimeCo
649662
);
650663

651664
const functionsDir = join(input.projectRoot, functionsDirName);
665+
const functionsRoot = resolve(functionsDir);
666+
const containerFunctionsRoot = toDockerPath(functionsRoot);
667+
const canonicalFunctionsRoot = yield* Effect.promise(() =>
668+
realpath(functionsRoot).catch(() => functionsRoot),
669+
);
670+
const pathMappings = [
671+
{ hostRoot: functionsRoot, containerRoot: containerFunctionsRoot },
672+
...(canonicalFunctionsRoot === functionsRoot
673+
? []
674+
: [{ hostRoot: canonicalFunctionsRoot, containerRoot: containerFunctionsRoot }]),
675+
];
652676
const functionBinds = new Map<string, DockerBind>();
653677
const emittedScopeWarnings = new Set<string>();
654678
const functionsConfig: Record<string, ServeFunctionContainerConfig> = {};
@@ -692,7 +716,7 @@ export const startEdgeRuntimeContainer = Effect.fn("functions.startEdgeRuntimeCo
692716
input.discoverFunctionEnvFiles && Option.isNone(input.envFile)
693717
? yield* parseFunctionEnvFile(join(functionsDir, config.slug, ".env"))
694718
: {};
695-
functionsConfig[config.slug] = toFunctionContainerConfig(config, functionEnv);
719+
functionsConfig[config.slug] = toFunctionContainerConfig(config, functionEnv, pathMappings);
696720
}
697721

698722
const binds = [...functionBinds.values()];
@@ -716,7 +740,7 @@ export const startEdgeRuntimeContainer = Effect.fn("functions.startEdgeRuntimeCo
716740
`SUPABASE_INTERNAL_JWT_SECRET=${input.authArtifacts.jwtSecret}`,
717741
`SUPABASE_JWKS=${input.authArtifacts.jwks}`,
718742
`SUPABASE_INTERNAL_HOST_PORT=${input.config.apiPort}`,
719-
`SUPABASE_INTERNAL_FUNCTIONS_ROOT=${toDockerPath(functionsDir)}`,
743+
`SUPABASE_INTERNAL_FUNCTIONS_ROOT=${containerFunctionsRoot}`,
720744
`SUPABASE_INTERNAL_FUNCTIONS_CONFIG=${JSON.stringify(functionsConfig)}`,
721745
...(input.debug ? ["SUPABASE_INTERNAL_DEBUG=true"] : []),
722746
];

0 commit comments

Comments
 (0)