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
1 change: 1 addition & 0 deletions apps/cli/src/legacy/commands/start/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ not implemented.
| `BITBUCKET_CLONE_DIR` | When non-empty, drops named volumes and `--security-opt` from every container create | no |
| `DOCKER_HOST` / `DOCKER_CONTEXT` / `DOCKER_TLS_VERIFY` / `DOCKER_CERT_PATH` / `DOCKER_API_VERSION` / `DOCKER_CONFIG` | Read (ambient shell OR a project `.env`/`.env.<env>`/`.env.local` file) to discover the Docker daemon this whole command talks to; `DOCKER_HOST` is also re-derived and set on Vector's container env so it can reach the host's Docker socket for log collection | no |
| `KONG_NGINX_WORKER_PROCESSES` | Read (ambient shell or project dotenv) into Kong's own container env (defaults to `"1"` when unset) | no |
| `HTTP_PROXY` / `http_proxy` / `HTTPS_PROXY` / `https_proxy` / `NO_PROXY` / `no_proxy` | Bun proxy settings. After project dotenv and container creation, `start` appends `localhost,127.0.0.1,[::1]` to the effective no-proxy value before local Kong probes and seeding; it never changes project/container env and ends with this CLI process. | no |

`docker`/`podman` must be resolvable on `PATH` — same fallback behavior as `stop`/`status`.

Expand Down
3 changes: 3 additions & 0 deletions apps/cli/src/legacy/commands/start/start.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
legacyIsEncryptedSecret,
} from "../../shared/legacy-vault-decrypt.ts";
import { legacyParseGoDuration } from "../../shared/legacy-go-duration.ts";
import { legacyConfigureLoopbackProxyBypass } from "../../shared/legacy-hostname.ts";
import {
legacyCliProjectFilterValue,
legacyServiceContainerIds,
Expand Down Expand Up @@ -1979,6 +1980,8 @@ export const legacyStart = Effect.fn("legacy.start")(function* (flags: LegacySta
projectRef: "",
config: effectiveLocalStorageConfig,
});
// Keep the synthetic value out of project dotenv resolution and container environments.
legacyConfigureLoopbackProxyBypass();
Comment thread
7ttp marked this conversation as resolved.
Comment thread
7ttp marked this conversation as resolved.
const healthResult = yield* legacyWaitForHealthyServices(spawner, [...started.keys()], {
postgrest: postgrestGateway,
edgeRuntime: edgeRuntimeGateway,
Expand Down
64 changes: 64 additions & 0 deletions apps/cli/src/legacy/commands/start/start.live.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { execFile } from "node:child_process";
import { once } from "node:events";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { createServer } from "node:net";
import { tmpdir } from "node:os";
import path from "node:path";
import { promisify } from "node:util";
Expand Down Expand Up @@ -201,6 +203,68 @@ describeLive("supabase start (live)", () => {
},
);

test(
"bypasses an HTTPS proxy for loopback gateway health checks",
{ timeout: START_TIMEOUT_MS + LIFECYCLE_OVERHEAD_MS },
async () => {
projectDir = await mkdtemp(path.join(tmpdir(), "sb-start-live-proxy-"));

const init = await runSupabaseLive(["init"], {
cwd: projectDir,
exitTimeoutMs: SHORT_LIVE_TIMEOUT_MS,
});
expect(init.exitCode, `stdout:\n${init.stdout}\nstderr:\n${init.stderr}`).toBe(0);

let proxyConnections = 0;
const proxy = createServer((socket) => {
proxyConnections += 1;
socket.destroy();
});

try {
proxy.listen(0, "127.0.0.1");
await once(proxy, "listening");
const address = proxy.address();
if (address === null || typeof address === "string") {
throw new Error("Failed to allocate a proxy port");
}

const excludeArgs = LEGACY_SERVICE_CATALOG.flatMap((entry) =>
entry.excludeKey === undefined ||
entry.excludeKey === "kong" ||
entry.excludeKey === "postgrest"
? []
: ["--exclude", entry.excludeKey],
);
const proxyUrl = `http://127.0.0.1:${address.port}`;
const start = await runSupabaseLive(["start", ...excludeArgs], {
cwd: projectDir,
exitTimeoutMs: START_TIMEOUT_MS,
env: {
HTTP_PROXY: "",
http_proxy: "",
HTTPS_PROXY: proxyUrl,
https_proxy: proxyUrl,
NO_PROXY: "",
no_proxy: "",
SUPABASE_API_TLS_ENABLED: "true",
SUPABASE_SERVICES_HOSTNAME: "127.0.0.1",
},
});

expect(start.exitCode, `stdout:\n${start.stdout}\nstderr:\n${start.stderr}`).toBe(0);
expect(start.stdout).toContain("https://127.0.0.1:");
expect(proxyConnections).toBe(0);
} finally {
if (proxy.listening) {
await new Promise<void>((resolve, reject) => {
proxy.close((error) => (error === undefined ? resolve() : reject(error)));
});
}
}
},
);

// The health watch inspects and dumps logs by container NAME against a real
// daemon, and derives recovery advice from a real container's real log bytes.
// Neither is observable through the in-process mocks, so this reproduces
Expand Down
8 changes: 8 additions & 0 deletions apps/cli/src/legacy/shared/legacy-hostname.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { homedir } from "node:os";
import { join } from "node:path";

const LOCAL_HOST = "127.0.0.1";
const LOOPBACK_NO_PROXY = `localhost,${LOCAL_HOST},[::1]`;

/** Docker CLI's reserved "no context store entry" name (`docker/cli` `cli/command/cli.go`'s `DefaultContextName`). */
const DEFAULT_CONTEXT_NAME = "default";
Expand Down Expand Up @@ -141,3 +142,10 @@ export function legacyGetHostname(): string {
}
return LOCAL_HOST;
}

/** Keeps Bun from proxying the legacy CLI's loopback HTTP requests. */
export function legacyConfigureLoopbackProxyBypass(env: NodeJS.ProcessEnv = process.env): void {
const key = (env["no_proxy"]?.length ?? 0) > 0 ? "no_proxy" : "NO_PROXY";
Comment thread
7ttp marked this conversation as resolved.
const current = env[key];
env[key] = current ? `${current},${LOOPBACK_NO_PROXY}` : LOOPBACK_NO_PROXY;
}
32 changes: 31 additions & 1 deletion apps/cli/src/legacy/shared/legacy-hostname.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";

import { legacyGetHostname } from "./legacy-hostname.ts";
import { legacyConfigureLoopbackProxyBypass, legacyGetHostname } from "./legacy-hostname.ts";

const LOOPBACK_NO_PROXY = "localhost,127.0.0.1,[::1]";

function withEnv<T>(entries: Record<string, string | undefined>, run: () => T): T {
const previous: Record<string, string | undefined> = {};
Expand Down Expand Up @@ -186,3 +188,31 @@ describe("legacyGetHostname", () => {
});
});
});

describe("legacyConfigureLoopbackProxyBypass", () => {
it.each([
["sets NO_PROXY when neither spelling is configured", {}, { NO_PROXY: LOOPBACK_NO_PROXY }],
[
"preserves an existing NO_PROXY value",
{ NO_PROXY: "example.com" },
{ NO_PROXY: `example.com,${LOOPBACK_NO_PROXY}` },
],
[
"updates the non-empty lowercase value preferred by Bun",
{ NO_PROXY: "uppercase.example", no_proxy: "lowercase.example" },
{
NO_PROXY: "uppercase.example",
no_proxy: `lowercase.example,${LOOPBACK_NO_PROXY}`,
},
],
[
"falls back to NO_PROXY when lowercase no_proxy is empty",
{ NO_PROXY: "example.com", no_proxy: "" },
{ NO_PROXY: `example.com,${LOOPBACK_NO_PROXY}`, no_proxy: "" },
],
])("%s", (_name, env, expected) => {
legacyConfigureLoopbackProxyBypass(env);

expect(env).toEqual(expected);
});
});
Loading