Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,33 @@ function extractFlagValues(args: ReadonlyArray<string>, flag: string) {
return args.flatMap((value, index) => (args[index - 1] === flag ? [value] : []));
}

function withConfiguredTempDir<A, E, R>(
directory: string,
effect: Effect.Effect<A, E, R>,
): Effect.Effect<A, E, R> {
const names = ["TMPDIR", "TMP", "TEMP"] as const;
const previous = names.map((name) => [name, process.env[name]] as const);

return Effect.sync(() => {
for (const name of names) {
process.env[name] = directory;
}
}).pipe(
Effect.andThen(effect),
Effect.ensuring(
Effect.sync(() => {
for (const [name, value] of previous) {
if (value === undefined) {
delete process.env[name];
} else {
process.env[name] = value;
}
}
}),
),
);
}

async function extractDockerEnvEntries(call: { args: ReadonlyArray<string>; options: unknown }) {
const values = extractFlagValues(call.args, "-e");
if (values.some((value) => value.includes("="))) {
Expand Down Expand Up @@ -637,6 +664,92 @@ describe("legacy functions serve integration", () => {
});
});

it.live("creates a missing configured temp directory for runtime artifacts", () => {
const configuredTempDir = join(tempRoot.current, "missing-temp");
const multilineValue = "line-1\nline-2";

return Effect.gen(function* () {
yield* Effect.promise(() =>
writeProjectConfig(['project_id = "test-project"', ""].join("\n")),
);
yield* Effect.promise(() =>
writeFunctionFile("hello", "index.ts", 'Deno.serve(() => new Response("hello"))\n'),
);
yield* Effect.promise(() =>
writeProjectFile(
".env.local",
[`SINGLE_LINE=value`, `MULTILINE_VALUE="${multilineValue}"`, ""].join("\n"),
),
);

const { layer } = setupServe();
const error = yield* withConfiguredTempDir(
configuredTempDir,
legacyFunctionsServe(
baseFlags({
envFile: Option.some(".env.local"),
noVerifyJwt: Option.some(true),
}),
).pipe(Effect.provide(layer), Effect.flip),
);

expect(error).toBeInstanceOf(Error);
expect(existsSync(configuredTempDir)).toBe(true);

const dockerRun = deployMockState.runCalls.find(
(call) => call.command === "docker" && call.args[0] === "run",
);
expect(dockerRun).toBeDefined();
if (dockerRun === undefined) {
throw new Error("expected docker run call");
}

const envs = yield* Effect.promise(() => extractDockerEnvEntries(dockerRun));
expect(envs).toContain("SINGLE_LINE=value");

const options =
typeof dockerRun.options === "object" && dockerRun.options !== null
? dockerRun.options
: undefined;
const multilineEnvFiles =
options !== undefined && "multilineEnvFiles" in options
? (options.multilineEnvFiles as Record<string, string> | undefined)
: undefined;
expect(multilineEnvFiles).toEqual({ "env-0": multilineValue });
});
});

it.live("surfaces filesystem errors for an unusable configured temp path", () => {
const configuredTempPath = join(tempRoot.current, "temp-file");

return Effect.gen(function* () {
yield* Effect.promise(() =>
writeProjectConfig(['project_id = "test-project"', ""].join("\n")),
);
yield* Effect.promise(() =>
writeFunctionFile("hello", "index.ts", 'Deno.serve(() => new Response("hello"))\n'),
);
yield* Effect.promise(() => writeFile(configuredTempPath, "not a directory"));

const { layer } = setupServe();
const error = yield* withConfiguredTempDir(
configuredTempPath,
legacyFunctionsServe(baseFlags()).pipe(Effect.provide(layer), Effect.flip),
);

expect(error).toBeInstanceOf(Error);
if (error instanceof Error) {
expect(error.message).toContain(configuredTempPath);
expect(error.message).not.toContain("An error occurred in Effect.tryPromise");
}
expect(
deployMockState.runCalls.filter(
(call) => call.command === "docker" && call.args[0] === "run",
),
).toHaveLength(0);
});
});

it.live("fails before startup when a multiline env name is not a shell identifier", () => {
return Effect.gen(function* () {
yield* Effect.promise(() =>
Expand Down
31 changes: 21 additions & 10 deletions apps/cli/src/shared/functions/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -818,13 +818,19 @@ function splitEnvEntry(entry: string) {
: ([entry.slice(0, separatorIndex), entry.slice(separatorIndex + 1)] as const);
}

async function createFunctionsServeTempDir(prefix: string) {
const root = tmpdir();
await mkdir(root, { recursive: true });
return mkdtemp(join(root, prefix));
}

async function writeDockerEnvFile(env: Readonly<Record<string, string>>) {
const entries = Object.entries(env);
if (entries.length === 0) {
return undefined;
}

const dir = await mkdtemp(join(tmpdir(), "supabase-functions-serve-env-"));
const dir = await createFunctionsServeTempDir("supabase-functions-serve-env-");
const path = join(dir, "docker.env");
// The file holds the JWT secret, anon/service-role keys, and JWKS, so keep it
// owner-only rather than relying on the process umask.
Expand All @@ -850,7 +856,7 @@ async function writeDockerMultilineEnvScript(
return undefined;
}

const dir = await mkdtemp(join(tmpdir(), "supabase-functions-serve-multiline-env-"));
const dir = await createFunctionsServeTempDir("supabase-functions-serve-multiline-env-");
const scriptName = "multiline-env.sh";
const path = join(dir, scriptName);
const envDir = join(containerDir, "values");
Expand Down Expand Up @@ -1252,7 +1258,7 @@ export function buildServeEntrypointCommand(
async function writeServeMainTemplateFile(template: string) {
// Mount the bundled runtime template instead of embedding it in `sh -c` so
// Windows does not hit `uv_spawn` ENAMETOOLONG on path-heavy projects.
const dir = await mkdtemp(join(tmpdir(), "supabase-functions-serve-main-"));
const dir = await createFunctionsServeTempDir("supabase-functions-serve-main-");
const pathname = join(dir, "index.ts");
await writeFile(pathname, template);
return {
Expand Down Expand Up @@ -1412,11 +1418,15 @@ const startEdgeRuntime = Effect.fnUntraced(function* (input: {
try: () => validateDockerMultilineEnvNames(multilineDockerEnv),
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
});
const dockerEnvFile = yield* Effect.tryPromise(() => writeDockerEnvFile(singleLineDockerEnv));
const dockerEnvFile = yield* Effect.tryPromise({
try: () => writeDockerEnvFile(singleLineDockerEnv),
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
});
const multilineEnvDir = "/root/.supabase/multiline-env";
const dockerMultilineEnvScript = yield* Effect.tryPromise(() =>
writeDockerMultilineEnvScript(multilineDockerEnv, multilineEnvDir),
).pipe(Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))));
const dockerMultilineEnvScript = yield* Effect.tryPromise({
try: () => writeDockerMultilineEnvScript(multilineDockerEnv, multilineEnvDir),
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
});

const labels = dockerProjectLabels(projectId);
const runtimeCommand = [
Expand All @@ -1429,9 +1439,10 @@ const startEdgeRuntime = Effect.fnUntraced(function* (input: {
...(input.debug ? ["--verbose"] : []),
];
const serveMainTemplate = yield* Effect.promise(() => getLegacyFunctionsServeMainTemplate());
const serveMainTemplateFile = yield* Effect.tryPromise(() =>
writeServeMainTemplateFile(serveMainTemplate),
).pipe(Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))));
const serveMainTemplateFile = yield* Effect.tryPromise({
try: () => writeServeMainTemplateFile(serveMainTemplate),
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
});
const command = [
"run",
"-d",
Expand Down
Loading