Skip to content
Open
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
8 changes: 4 additions & 4 deletions apps/cli-go/pkg/config/templates/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Exposed for updates by .github/dependabot.yml
FROM supabase/postgres:17.6.1.165 AS pg
FROM supabase/postgres:17.6.1.167 AS pg
# Append to ServiceImages when adding new dependencies below
FROM library/kong:2.8.1 AS kong
FROM axllent/mailpit:v1.30.2 AS mailpit
Expand All @@ -9,10 +9,10 @@ FROM supabase/studio:2026.08.24-sha-8ec45b2 AS studio
FROM darthsim/imgproxy:v3.8.0 AS imgproxy
FROM supabase/edge-runtime:v1.74.3 AS edgeruntime
FROM timberio/vector:0.53.0-alpine AS vector
FROM supabase/supavisor:2.9.7 AS supavisor
FROM supabase/supavisor:2.9.12 AS supavisor
FROM supabase/gotrue:v2.196.0 AS gotrue
FROM supabase/realtime:v2.129.9 AS realtime
FROM supabase/storage-api:v1.71.0 AS storage
FROM supabase/realtime:v2.130.0 AS realtime
FROM supabase/storage-api:v1.72.1 AS storage
FROM supabase/logflare:1.50.6 AS logflare
# Append to JobImages when adding new dependencies below
FROM supabase/pgadmin-schema-diff:cli-0.0.5 AS differ
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { describe, expect, it } from "@effect/vitest";
import { Cause, Effect, Exit, Layer, Option } from "effect";
import * as HttpClient from "effect/unstable/http/HttpClient";
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
import { afterEach, vi } from "vitest";

import {
mockLegacyCliSettings,
Expand All @@ -28,6 +29,7 @@ import {
type LegacyEdgeRuntimeRunOpts,
LegacyEdgeRuntimeScript,
} from "../../../shared/legacy-edge-runtime-script.service.ts";
import { dockerfileServiceImageRaw } from "../../../../shared/services/dockerfile-images.ts";
import { LEGACY_SUGGEST_DOCKER_INSTALL } from "../../../shared/legacy-docker-suggest.ts";
import { LegacyPgDeltaSslProbe } from "../../../shared/legacy-pgdelta-ssl-probe.service.ts";
import { LegacyDeclarativeShadowDbError } from "./legacy-pgdelta.errors.ts";
Expand Down Expand Up @@ -115,12 +117,17 @@ const sslProbe = Layer.succeed(LegacyPgDeltaSslProbe, {

function setup(
workdir: string,
opts: { readonly failCreate?: boolean; readonly dbInspectFailsWith?: string } = {},
opts: {
readonly failCreate?: boolean;
readonly dbInspectFailsWith?: string;
readonly dbInspectImage?: string;
} = {},
) {
const out = mockOutput();
const shadowSpawner = mockLegacyShadowContainerCliSpawner({
failCreate: opts.failCreate,
dbInspectFailsWith: opts.dbInspectFailsWith,
dbInspectImage: opts.dbInspectImage,
});
const dbConnection = fakeShadowDbConnection();
const docker = fakeShadowSetupDocker();
Expand Down Expand Up @@ -272,3 +279,40 @@ describe("legacyDeclarativeSeamLayer.ensureLocalDatabaseStarted", () => {
},
);
});

describe("legacyDeclarativeSeamLayer.ensureLocalPostgresImageCurrent", () => {
afterEach(() => {
vi.unstubAllEnvs();
});

it.effect(
"flags a running docker.io container as stale against a slim-flagged expectation, even on a matching tag",
() => {
vi.stubEnv("SUPABASE_USE_SLIM_IMAGES", "true");
const dir = mkdtempSync(join(tmpdir(), "legacy-pgdelta-seam-"));
const { layer } = setup(dir, { dbInspectImage: dockerfileServiceImageRaw("pg") });
return Effect.gen(function* () {
const seam = yield* LegacyDeclarativeSeam;
const exit = yield* seam.ensureLocalPostgresImageCurrent().pipe(Effect.exit);
expect(Exit.isFailure(exit)).toBe(true);
const error = failError(exit);
expect(error).toBeInstanceOf(LegacyDeclarativeShadowDbError);
expect((error as LegacyDeclarativeShadowDbError).message).toContain(
"local Postgres container image is stale",
);
rmSync(dir, { recursive: true, force: true });
}).pipe(Effect.provide(layer));
},
);

it.effect("passes when the running container matches the expected image's family and tag", () => {
const dir = mkdtempSync(join(tmpdir(), "legacy-pgdelta-seam-"));
const { layer } = setup(dir, { dbInspectImage: dockerfileServiceImageRaw("pg") });
return Effect.gen(function* () {
const seam = yield* LegacyDeclarativeSeam;
const exit = yield* seam.ensureLocalPostgresImageCurrent().pipe(Effect.exit);
expect(Exit.isSuccess(exit)).toBe(true);
rmSync(dir, { recursive: true, force: true });
}).pipe(Effect.provide(layer));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { legacyResolveDbImage } from "../../../shared/legacy-db-image.ts";
import { legacyReadDbToml } from "../../../shared/legacy-db-config.toml-read.ts";
import { legacyGetRegistryImageUrl } from "../../../shared/legacy-docker-registry.ts";
import { legacyIsDockerDaemonUnreachable } from "../../../shared/legacy-docker-suggest.ts";
import { isSlimImageRef } from "../../../../shared/services/slim-images.ts";
import { legacyIsLocalDbRunning } from "../../../shared/db-bootstrap/local-db-running.ts";
import { legacyStartLocalDatabase } from "../../../shared/db-bootstrap/start-local-database.ts";
import {
Expand Down Expand Up @@ -262,7 +263,14 @@ export const legacyDeclarativeSeamLayer = Layer.effect(
const expected = legacyGetRegistryImageUrl(image).trim();
const actualTag = dockerImageTag(actual);
const expectedTag = dockerImageTag(expected);
if (actualTag.length === 0 || expectedTag.length === 0 || actualTag === expectedTag) {
// Slim refs never go through a registry mirror, so a family mismatch
// (e.g. a docker.io container satisfying a ghcr.io/supabase/cli
// expectation) is stale even when the tags happen to match.
const familyMismatch = isSlimImageRef(expected) !== isSlimImageRef(actual);
if (
!familyMismatch &&
(actualTag.length === 0 || expectedTag.length === 0 || actualTag === expectedTag)
) {
return;
}
return yield* Effect.fail(
Expand Down
20 changes: 20 additions & 0 deletions apps/cli/src/legacy/commands/db/start/start.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ const currentBranchPath = (workdir: string) =>
describe("legacy db start", () => {
afterEach(() => {
delete process.env["SUPABASE_NETWORK_ID"];
vi.unstubAllEnvs();
});

it.live("reports an already-running database without starting a container", () => {
Expand Down Expand Up @@ -682,6 +683,25 @@ describe("legacy db start", () => {
},
);

it.live(
"--from-backup under SUPABASE_USE_SLIM_IMAGES uses the same restore entrypoint as docker.io",
() => {
vi.stubEnv("SUPABASE_USE_SLIM_IMAGES", "1");
const { layer, child } = setup({ route: freshVolumeRoute(defaultRoute()) });
return Effect.gen(function* () {
yield* legacyDbStart(flags("/abs/host/backup.sql")).pipe(Effect.provide(layer));
const args = createArgs(child.spawned);
expect(args).not.toBeUndefined();
const script = args?.[(args?.indexOf("-c") ?? -1) + 1];
expect(script).toContain("/docker-entrypoint-initdb.d/migrate.sh");
expect(bindsFromCreateArgs(args ?? [])).toContain(
"/abs/host/backup.sql:/etc/backup.sql:ro",
);
expect(dbSetupJobCalls(child.spawned)).toHaveLength(0);
}).pipe(Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())));
},
);

it.live("resolves a relative --from-backup against the caller cwd, not the workdir", () => {
const { layer, child } = setup({
route: freshVolumeRoute(defaultRoute()),
Expand Down
3 changes: 2 additions & 1 deletion apps/cli/src/legacy/commands/gen/types/types.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,13 +440,14 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le
// `--network-id` overrides any base network mode (even the
// "host" mode used for --db-url), so honour the override here too.
const networkMode = Option.isSome(networkId) ? networkId.value : input.networkMode;
const pgmetaImage = resolvePgmetaImage(input.pgmetaVersionOverride);
const args = [
"run",
"--rm",
"--network",
networkMode,
...env.flatMap((entry) => ["--env", entry]),
resolvePgmetaImage(input.pgmetaVersionOverride),
pgmetaImage,
"node",
"dist/server/server.js",
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2371,6 +2371,7 @@ describe("legacy gen types", () => {
true,
);
expect(child.spawned[1]?.args).toContain(resolvePgmetaImage());
expect(child.spawned[1]?.args.slice(-2)).toEqual(["node", "dist/server/server.js"]);
// The local/db-url paths have no project ref, so they must not
// populate the linked-project cache.
expect(linkedProjectCache.cached).toBe(false);
Expand Down
22 changes: 6 additions & 16 deletions apps/cli/src/legacy/commands/gen/types/types.shared.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Effect } from "effect";
import { dockerfileServiceImage } from "../../../../shared/services/dockerfile-images.ts";
import { dockerfileServiceImageRaw } from "../../../../shared/services/dockerfile-images.ts";
import { slimImageForCurrentPin } from "../../../../shared/services/slim-images.ts";
import { legacyGetRegistryImageUrl } from "../../../shared/legacy-docker-registry.ts";
import {
LegacyInvalidGenTypesDatabaseUrlError,
Expand Down Expand Up @@ -140,23 +141,12 @@ export function buildPostgresUrl(input: {
}

export function resolvePgmetaImage(versionOverride?: string) {
const defaultImage = dockerfileServiceImage("pgmeta");
if (versionOverride === undefined || versionOverride.trim().length === 0) {
return legacyGetRegistryImageUrl(defaultImage);
}
return legacyGetRegistryImageUrl(
replaceImageTag(defaultImage, `v${versionOverride.trim().replace(/^v/i, "")}`),
);
const raw = dockerfileServiceImageRaw("pgmeta");
const trimmed = versionOverride?.trim() ?? "";
const pin = trimmed.length > 0 ? `v${trimmed.replace(/^v/i, "")}` : undefined;
return legacyGetRegistryImageUrl(slimImageForCurrentPin("pgmeta", raw, pin));
}

export function legacyRootCaBundle() {
return `${caStaging2021}${caProd2021}${caProd2025}`;
}

function replaceImageTag(image: string, tag: string): string {
const tagSeparator = image.lastIndexOf(":");
if (tagSeparator === -1) {
return image;
}
return `${image.slice(0, tagSeparator + 1)}${tag}`;
}
21 changes: 21 additions & 0 deletions apps/cli/src/legacy/commands/gen/types/types.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { describe, expect, it } from "@effect/vitest";
import { Effect, Exit } from "effect";
import { dockerfileServiceImageRaw } from "../../../../shared/services/dockerfile-images.ts";
import { toSlimImage } from "../../../../shared/services/slim-images.ts";
import { legacyGetHostname } from "../../../shared/legacy-hostname.ts";
import { legacyParseSchemaFlags } from "../../../shared/legacy-schema-flags.ts";
import {
Expand All @@ -14,6 +16,9 @@ import {
resolvePgmetaImage,
} from "./types.shared.ts";

const currentPgmeta = dockerfileServiceImageRaw("pgmeta");
const currentPgmetaTag = currentPgmeta.split(":")[1] ?? "";

function withEnv<T>(key: string, value: string | undefined, run: () => T): T {
const previous = process.env[key];
if (value === undefined) {
Expand Down Expand Up @@ -180,6 +185,22 @@ describe("resolvePgmetaImage", () => {
);
expect(image).toBe("my.registry.example/supabase/postgres-meta:v1.2.3");
});

it("slim-translates the current pin and skips registry rewrite", () => {
const image = withEnv("SUPABASE_USE_SLIM_IMAGES", "1", () =>
withEnv("SUPABASE_INTERNAL_IMAGE_REGISTRY", undefined, () =>
resolvePgmetaImage(currentPgmetaTag),
),
);
expect(image).toBe(toSlimImage("pgmeta", currentPgmeta));
});

it("keeps a historical pg-meta pin on docker.io under the slim flag", () => {
const image = withEnv("SUPABASE_USE_SLIM_IMAGES", "1", () =>
withEnv("SUPABASE_INTERNAL_IMAGE_REGISTRY", "docker.io", () => resolvePgmetaImage("1.2.3")),
);
expect(image).toBe("supabase/postgres-meta:v1.2.3");
});
});

describe("schema and id helpers", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export const legacyServices = Effect.fn("legacy.services")(function* (_flags: Le
imageOverrides,
normalizeVersionTags: false,
serviceVersions,
slimCurrentPinOnly: true,
};

let rows = listLocalServiceVersions(localImageOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { describe, expect, it } from "@effect/vitest";
import { edgeRuntimeNofileUlimit } from "@supabase/stack/effect";
import { Deferred, Effect, Exit, Sink, Stream } from "effect";
import { type ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
import { beforeEach } from "vitest";
import { afterEach, beforeEach, vi } from "vitest";

import { useLegacyTempWorkdir } from "../../../../../tests/helpers/legacy-mocks.ts";
import { mockOutput } from "../../../../../tests/helpers/mocks.ts";
Expand Down Expand Up @@ -130,6 +130,10 @@ describe("legacyStartEdgeRuntimeContainer", () => {
mkdirSync(join(tempWorkdir.current, "supabase", "functions"), { recursive: true });
});

afterEach(() => {
vi.unstubAllEnvs();
});

it.effect(
"sends the real internal db url (db container name, port 5432, config.db.password) — NOT functions serve's `db`-alias default",
() =>
Expand Down Expand Up @@ -346,6 +350,50 @@ describe("legacyStartEdgeRuntimeContainer", () => {
}),
);

it.effect(
"slim edge-runtime uses the docker.io entrypoint, /root main service, and shared cache volume",
() =>
Effect.gen(function* () {
vi.stubEnv("SUPABASE_USE_SLIM_IMAGES", "1");
const mock = mockDockerSpawner();
const out = mockOutput();
const input = {
...baseInput(tempWorkdir.current),
image: "ghcr.io/supabase/cli/edge-runtime:v1.74.2",
};

yield* legacyStartEdgeRuntimeContainer(input).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, mock.spawner),
Effect.provide(out.layer),
);

const createArgs = mock.runCall!.args;
expect(createArgs).toContain("--entrypoint");
expect(createArgs).toContain("sh");
const script = createArgs.at(-1);
expect(script).toContain("--main-service=/root");
expect(script).not.toContain("--main-service=/tmp");

const volumeCreate = mock.calls.find((call) => call.args[0] === "volume");
expect(volumeCreate?.args.at(-1)).toBe("supabase_edge_runtime_proj");
expect(createArgs).not.toContain("supabase_edge_runtime_slim_proj:/home/nonroot:rw");

const cp = mock.calls.find((call) => call.args[0] === "cp");
expect(cp?.args).toEqual(["cp", "-", "supabase_edge_runtime_proj:/"]);
const stdin = cp?.stdin;
expect(Stream.isStream(stdin)).toBe(true);
if (!Stream.isStream(stdin)) return yield* Effect.die("docker cp stdin was not a stream");
const chunks = yield* Stream.runCollect(stdin);
expect(chunks).toHaveLength(1);
const archiveBytes = chunks[0];
if (!(archiveBytes instanceof Uint8Array)) {
return yield* Effect.die("docker cp stdin did not contain archive bytes");
}
const files = yield* Effect.promise(() => new Bun.Archive(archiveBytes).files());
expect([...files.keys()]).toEqual(["root/index.ts"]);
}),
);

it.effect(
"surfaces docker's own stderr verbatim and never reaches cp/start when docker create fails",
() =>
Expand Down
Loading
Loading