diff --git a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote-with-project-id-env.json b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote-with-project-id-env.json index 2795fb7dae..c6dc82d42a 100644 --- a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote-with-project-id-env.json +++ b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote-with-project-id-env.json @@ -5,8 +5,8 @@ "dbHealthTimeoutSeconds": 120, "majorVersion": 17, "orioledbVersion": null, - "postgresConfigImage": "supabase/postgres:17.11.0.005", - "postgresImage": "supabase/postgres:17.11.0.005", + "postgresConfigImage": "supabase/postgres:17.11.0.006", + "postgresImage": "supabase/postgres:17.11.0.006", "realtimeEnabledForSetup": true, "realtimeIpVersion": "IPv4", "realtimeMaxHeaderLength": 4096, @@ -32,7 +32,7 @@ "experimental": false, "networkId": "supabase_network_shell-project-id", "postgresSpecBase": { - "configImage": "supabase/postgres:17.11.0.005", + "configImage": "supabase/postgres:17.11.0.006", "db": { "health_timeout": "2m", "major_version": 17, diff --git a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote-with-shell-env.json b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote-with-shell-env.json index 8a6995b84d..3342de19a8 100644 --- a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote-with-shell-env.json +++ b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote-with-shell-env.json @@ -5,8 +5,8 @@ "dbHealthTimeoutSeconds": 120, "majorVersion": 17, "orioledbVersion": null, - "postgresConfigImage": "supabase/postgres:17.11.0.005", - "postgresImage": "supabase/postgres:17.11.0.005", + "postgresConfigImage": "supabase/postgres:17.11.0.006", + "postgresImage": "supabase/postgres:17.11.0.006", "realtimeEnabledForSetup": false, "realtimeIpVersion": "IPv4", "realtimeMaxHeaderLength": 4096, @@ -32,7 +32,7 @@ "experimental": false, "networkId": "supabase_network_abcdefghijklmnopqrst", "postgresSpecBase": { - "configImage": "supabase/postgres:17.11.0.005", + "configImage": "supabase/postgres:17.11.0.006", "db": { "health_timeout": "2m", "major_version": 17, diff --git a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote.json b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote.json index d29f807966..4431a2fa44 100644 --- a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote.json +++ b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote.json @@ -5,8 +5,8 @@ "dbHealthTimeoutSeconds": 120, "majorVersion": 17, "orioledbVersion": null, - "postgresConfigImage": "supabase/postgres:17.11.0.005", - "postgresImage": "supabase/postgres:17.11.0.005", + "postgresConfigImage": "supabase/postgres:17.11.0.006", + "postgresImage": "supabase/postgres:17.11.0.006", "realtimeEnabledForSetup": true, "realtimeIpVersion": "IPv4", "realtimeMaxHeaderLength": 4096, @@ -32,7 +32,7 @@ "experimental": false, "networkId": "supabase_network_abcdefghijklmnopqrst", "postgresSpecBase": { - "configImage": "supabase/postgres:17.11.0.005", + "configImage": "supabase/postgres:17.11.0.006", "db": { "health_timeout": "2m", "major_version": 17, diff --git a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/no-overrides.json b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/no-overrides.json index 8ce11560f8..ec13fc8df4 100644 --- a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/no-overrides.json +++ b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/no-overrides.json @@ -5,8 +5,8 @@ "dbHealthTimeoutSeconds": 120, "majorVersion": 17, "orioledbVersion": null, - "postgresConfigImage": "supabase/postgres:17.11.0.005", - "postgresImage": "supabase/postgres:17.11.0.005", + "postgresConfigImage": "supabase/postgres:17.11.0.006", + "postgresImage": "supabase/postgres:17.11.0.006", "realtimeEnabledForSetup": true, "realtimeIpVersion": "IPv4", "realtimeMaxHeaderLength": 4096, @@ -32,7 +32,7 @@ "experimental": false, "networkId": "supabase_network_golden-shadow", "postgresSpecBase": { - "configImage": "supabase/postgres:17.11.0.005", + "configImage": "supabase/postgres:17.11.0.006", "db": { "health_timeout": "2m", "major_version": 17, diff --git a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/project-dotenv.json b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/project-dotenv.json index af872b7d6d..d11d0fc2ae 100644 --- a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/project-dotenv.json +++ b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/project-dotenv.json @@ -5,8 +5,8 @@ "dbHealthTimeoutSeconds": 120, "majorVersion": 17, "orioledbVersion": null, - "postgresConfigImage": "supabase/postgres:17.11.0.005", - "postgresImage": "supabase/postgres:17.11.0.005", + "postgresConfigImage": "supabase/postgres:17.11.0.006", + "postgresImage": "supabase/postgres:17.11.0.006", "realtimeEnabledForSetup": false, "realtimeIpVersion": "IPv4", "realtimeMaxHeaderLength": 4096, @@ -32,7 +32,7 @@ "experimental": false, "networkId": "supabase_network_golden-shadow", "postgresSpecBase": { - "configImage": "supabase/postgres:17.11.0.005", + "configImage": "supabase/postgres:17.11.0.006", "db": { "health_timeout": "2m", "major_version": 17, diff --git a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/shell-env.json b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/shell-env.json index 5967ac757a..6ada9068d3 100644 --- a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/shell-env.json +++ b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/shell-env.json @@ -5,8 +5,8 @@ "dbHealthTimeoutSeconds": 120, "majorVersion": 17, "orioledbVersion": null, - "postgresConfigImage": "supabase/postgres:17.11.0.005", - "postgresImage": "supabase/postgres:17.11.0.005", + "postgresConfigImage": "supabase/postgres:17.11.0.006", + "postgresImage": "supabase/postgres:17.11.0.006", "realtimeEnabledForSetup": false, "realtimeIpVersion": "IPv4", "realtimeMaxHeaderLength": 4096, @@ -32,7 +32,7 @@ "experimental": false, "networkId": "supabase_network_golden-shadow", "postgresSpecBase": { - "configImage": "supabase/postgres:17.11.0.005", + "configImage": "supabase/postgres:17.11.0.006", "db": { "health_timeout": "2m", "major_version": 17, diff --git a/apps/cli/src/shared/services/Dockerfile b/apps/cli/src/shared/services/Dockerfile index c4ed418420..55ada23ba3 100644 --- a/apps/cli/src/shared/services/Dockerfile +++ b/apps/cli/src/shared/services/Dockerfile @@ -5,9 +5,9 @@ # a catalog update. Everything else in this file (this comment, kong, pg14, the one-shot job # images) is hand- or Dependabot-managed. Keep a line between image lines: git conflicts on edits to # adjacent lines, and catalog PRs for neighbouring aliases are often open at the same time. -FROM supabase/postgres:17.11.0.005 AS pg +FROM supabase/postgres:17.11.0.006 AS pg -FROM supabase/postgres:15.19.0.005 AS pg15 +FROM supabase/postgres:15.19.0.006 AS pg15 # Postgres 14 has no slim build; bumped by hand (Dependabot ignores supabase/postgres). FROM supabase/postgres:14.1.0.89 AS pg14 # New always-on service alias: extend SERVICE_IMAGE_ALIASES in diff --git a/packages/stack/src/Artifacts.ts b/packages/stack/src/Artifacts.ts index 8677ee5e70..25758c8157 100644 --- a/packages/stack/src/Artifacts.ts +++ b/packages/stack/src/Artifacts.ts @@ -114,23 +114,23 @@ const definitions: Readonly> = { database: definition( "postgres", { - upstreamVersion: "17.11.0.005", + upstreamVersion: "17.11.0.006", revision: 0, image: - "ghcr.io/supabase/cli/postgres:17.11.0.005-r0@sha256:7929054d92a7a0682f853af321bb65d7ffcf49123ac5578b4d276f05088211a8", - upstreamImage: "supabase/postgres:17.11.0.005", + "ghcr.io/supabase/cli/postgres:17.11.0.006-r0@sha256:ee0f2d9747fb9d3c0a29703684ec718f070384ad05bf0d92823ed02cabde8e5a", + upstreamImage: "supabase/postgres:17.11.0.006", natives: { "darwin-arm64": { - archive: "6dcc013929e31cfefc716af250409f10be09473e66d8db4a190dc34baefb032f", - manifest: "d2fbc88c26a5745ab7f39e4fd425e6eb0bd4ce8837635ecb896dac20b701c7d0", + archive: "fb7594525d712ab85b6da00d4d4854f8d0c9ba26b49b781a5fede0119362e917", + manifest: "b499127a253b15f362fa217301d490f5d61f4b201d1b6501de06a11c6347e192", }, "linux-amd64": { - archive: "c449f17e25f9b16f3d79636efba88a08379ea7e9d915f39c46d0db52a225c1a8", - manifest: "b7bca7e7e858bb3970319f50c9b9d43f6156d323d10144a73e5cc0ae1b7ef3ca", + archive: "f3a4389c92c550597cacf8c71c74af025b8e75259753c4e78bb5014e6f29e6f2", + manifest: "457949debc6c8b1d17dfaedcdef4883af3ff1ef7a15998f74d9511294be9a2fa", }, "linux-arm64": { - archive: "478258bb3e738a023a913aecc904c9837250c0f53b382ab9208394ab82f66470", - manifest: "608ea63e0a1735fdbd5fe68c3f8fcc6d5b1279cca96b59a407947356bfbf7138", + archive: "443bb6be464b78fcc9a1e07d94cc4e8a5f0f9cba3d4ca3d5d8a8563fa962fddb", + manifest: "fd0f71d0f42898657a69962475164f4446f8f732edfac248708593b36eec4fcf", }, }, }, @@ -144,24 +144,24 @@ const definitions: Readonly> = { "bin/psql", ], { - "15.19.0.005": { - upstreamVersion: "15.19.0.005", + "15.19.0.006": { + upstreamVersion: "15.19.0.006", revision: 0, image: - "ghcr.io/supabase/cli/postgres:15.19.0.005-r0@sha256:8c99be31392a22ba25f6a0fe03e772ff9e1141a76044bd28578bf245f6e93d44", - upstreamImage: "supabase/postgres:15.19.0.005", + "ghcr.io/supabase/cli/postgres:15.19.0.006-r0@sha256:dd5d757785930d2daf216ccc2ea871e2337657d11c42f7cc709309774fa663f1", + upstreamImage: "supabase/postgres:15.19.0.006", natives: { "darwin-arm64": { - archive: "f308f7727280b40641da4266cf93b4dfcc8f351ff5c085fdbf0316d6e628f7c3", - manifest: "d36777085898fe0ee4fbdd1b54dc971ed58ab314367493f5b74e4e5e7bd702d8", + archive: "0350c9ff12f84061f1a35de8218400fb8e6059dedd36d037bd01e9b7adde3e4b", + manifest: "a2d941b715b0fa09918353164f13fac47751973ab5ae4ceca978bb8263c41f62", }, "linux-amd64": { - archive: "c8734fe7c013712909de297abe1dfb28ccb74a2f6d71f0af187c074c242d5cce", - manifest: "569a9ec7f820c55a05ea84b9c09891a83b0d28f2e630a950e385ca59e34dd6ad", + archive: "3e0126f154c174a88c45a9fa496441e1a15e2f328e4ad3b61fcc02c9caead69c", + manifest: "af86fad4f63a9bafbc853e8291275d95762da609e9775129f7debbdcdd35c1bb", }, "linux-arm64": { - archive: "5839443e6674ce706e388a928878cd4e44c91943d44aa3417a3082495200c42c", - manifest: "e19dbcab90d4033452bca4de0916c83f6851162c09fb1a10233ad86ad7ef35d1", + archive: "ec1f08c01a110aa267c646a7ab69dc63e2e544c5d9d0c0536cfb974423280b3a", + manifest: "7527e460894f9ef76c57ec03359db5e8cc4d8fdb7043f727f4213344d9f3e76c", }, }, }, @@ -207,6 +207,27 @@ const definitions: Readonly> = { }, }, }, + "17.11.0.006-orioledb": { + upstreamVersion: "17.11.0.006-orioledb", + revision: 0, + image: + "ghcr.io/supabase/cli/postgres:17.11.0.006-orioledb-r0@sha256:d1b3cb9421c5448dd1d226856344ef7abdd828a6d27722a1de11b3c0f5c46fc7", + upstreamImage: "supabase/postgres:17.11.0.006-orioledb", + natives: { + "darwin-arm64": { + archive: "6e6a90857cf3f6637b22e182856b18425fe6c12af80ef065b6dce736ce5a914c", + manifest: "5433b222cc40036069ab60fdfdf1e468c2ca1268a01f505b6a835c62f770abb8", + }, + "linux-amd64": { + archive: "b7ea7bcd9953e4dec1af75d26e2a96cd753d8ebafb12f01c91d0baf0d2fbe6e2", + manifest: "280b0d449ae5ca09631cf362bd8c228abff638bafc832f39849a13828e66fc88", + }, + "linux-arm64": { + archive: "b5bd6e730b89c3c150107740e1489b0bc1ce2ea3e8f802470e4c2bd7f88570e2", + manifest: "0cde8aff915fb425db9dadffbd3c0307b420f4e1c2b1b3a1c0dc1849ef296c47", + }, + }, + }, }, ), rest: definition( diff --git a/packages/stack/src/services/ProcessRecipe.ts b/packages/stack/src/services/ProcessRecipe.ts index d50e60351d..e94e1cdd02 100644 --- a/packages/stack/src/services/ProcessRecipe.ts +++ b/packages/stack/src/services/ProcessRecipe.ts @@ -97,6 +97,8 @@ export interface ProcessRecipeSpec>; readonly healthPath: string; + /** The `ports` entry `healthPath` is probed on; defaults to `http`. */ + readonly healthPort?: string; readonly args: ( creation: C, endpoints: ReadonlyMap, @@ -626,12 +628,12 @@ export const makeProcessRecipe = type NativeAttempt = Effect.Success>; const readyCondition = (attempt: NativeAttempt, timeout: Duration.Input) => { - const http = attempt.selected.get("http"); - if (http === undefined) + const ready = attempt.selected.get(spec.healthPort ?? "http"); + if (ready === undefined) return Effect.fail(serviceError("health", "Recipe has no HTTP readiness endpoint")); return Effect.all( [ - readiness(deps.client, http, spec.healthPath, timeout), + readiness(deps.client, ready, spec.healthPath, timeout), spec.nativeReadinessOutput === undefined ? Effect.void : Deferred.await(attempt.output.bindReady), @@ -890,7 +892,7 @@ export const makeProcessRecipe = yield* Ref.set(endpoints, selected); yield* publishProcessLogs(launched, yield* output.part, context.scope); const runtime = runtimeFromContainer(launched); - const ready = selected.get("http"); + const ready = selected.get(spec.healthPort ?? "http"); const noReadinessEndpoint = Effect.fail( serviceError("health", "Recipe has no HTTP readiness endpoint"), ); diff --git a/packages/stack/src/services/Rest.integration.test.ts b/packages/stack/src/services/Rest.integration.test.ts index 037de7c641..baa98e8a88 100644 --- a/packages/stack/src/services/Rest.integration.test.ts +++ b/packages/stack/src/services/Rest.integration.test.ts @@ -251,6 +251,18 @@ describe("service catalog", () => { yield* sql.unsafe( "INSERT INTO public.catalog_native_probe (id, value) VALUES (1, 'catalog-native');", ); + yield* sql.unsafe(`CREATE OR REPLACE FUNCTION public.catalog_native_hook() RETURNS void +LANGUAGE plpgsql AS $$ +BEGIN + IF current_setting('request.headers', true)::json->>'x-app-secret' IS DISTINCT FROM 'expected' THEN + RAISE SQLSTATE 'PGRST' USING MESSAGE = '{"code":"APP_403","message":"Unauthorized."}', + DETAIL = '{"status":403,"headers":{}}'; + END IF; +END $$;`); + yield* sql.unsafe("GRANT EXECUTE ON FUNCTION public.catalog_native_hook() TO anon;"); + yield* sql.unsafe( + "ALTER ROLE supabase_admin SET pgrst.db_pre_request = 'public.catalog_native_hook';", + ); const restRecipe = yield* makeServiceRecipe( { service: "rest", @@ -271,6 +283,9 @@ describe("service catalog", () => { yield* rest.start; yield* rest.ready; const endpoint = yield* restRecipe.endpoint("http"); + const base = "http://" + httpHost(endpoint) + ":" + endpoint.port; + const rejected = yield* client.execute(HttpClientRequest.get(base + "/")); + expect(rejected.status).toBe(403); const token = yield* Effect.tryPromise(() => new SignJWT({ role: "anon" }) .setProtectedHeader({ alg: "HS256", typ: "JWT" }) @@ -279,9 +294,10 @@ describe("service catalog", () => { .sign(new TextEncoder().encode(secret)), ); const response = yield* client.execute( - HttpClientRequest.get( - "http://" + httpHost(endpoint) + ":" + endpoint.port + "/catalog_native_probe", - ).pipe(HttpClientRequest.setHeader("Authorization", "Bearer " + token)), + HttpClientRequest.get(base + "/catalog_native_probe").pipe( + HttpClientRequest.setHeader("Authorization", "Bearer " + token), + HttpClientRequest.setHeader("x-app-secret", "expected"), + ), ); const responseBody = yield* response.text; expect(response.status).toBe(200); diff --git a/packages/stack/src/services/Rest.ts b/packages/stack/src/services/Rest.ts index 3eac29824b..84207f835b 100644 --- a/packages/stack/src/services/Rest.ts +++ b/packages/stack/src/services/Rest.ts @@ -25,18 +25,21 @@ export interface Creation extends Schema.Schema.Type {} export const makeSpec = (): ProcessRecipeSpec => ({ service: "rest", executable: "bin/postgrest", - ports: { http: 3000 }, - healthPath: "/", + ports: { http: 3000, admin: 3001 }, + healthPort: "admin", + healthPath: "/ready", env: (creation, endpoints, container) => Effect.gen(function* () { const databaseUrl = yield* requiredInput("rest", "databaseUrl", creation.config.databaseUrl); const http = endpoints.get("http"); + const admin = endpoints.get("admin"); const jwtSecret = creation.config.jwks ?? creation.config.jwtSecret; return { DATABASE_URL: databaseUrl, PGRST_DB_URI: databaseUrl, PGRST_SERVER_HOST: container ? "0.0.0.0" : "127.0.0.1", ...(http === undefined ? {} : { PGRST_SERVER_PORT: String(http.port) }), + ...(admin === undefined ? {} : { PGRST_ADMIN_SERVER_PORT: String(admin.port) }), PGRST_DB_SCHEMAS: creation.config.schemas ?? "public,graphql_public", ...(creation.config.extraSearchPath === undefined ? {} diff --git a/packages/stack/src/services/ServiceIdentity.unit.test.ts b/packages/stack/src/services/ServiceIdentity.unit.test.ts index f8dcf8d4aa..be63b159ae 100644 --- a/packages/stack/src/services/ServiceIdentity.unit.test.ts +++ b/packages/stack/src/services/ServiceIdentity.unit.test.ts @@ -1,6 +1,7 @@ import { expect, it } from "@effect/vitest"; import { Effect, Schema } from "effect"; import * as Realtime from "./Realtime.ts"; +import type { ServiceEndpoint } from "./Recipe.ts"; import * as Rest from "./Rest.ts"; import * as Storage from "./Storage.ts"; import * as Studio from "./Studio.ts"; @@ -30,6 +31,26 @@ it("forwards a PostgREST HMAC secret when JWKS is omitted", () => { expect(env.PGRST_JWT_SECRET).toBe("active-hmac-secret"); }); +it.effect("probes PostgREST readiness on its admin server port", () => + Effect.gen(function* () { + const spec = Rest.makeSpec(); + const creation = yield* Schema.decodeEffect(Rest.Creation)({ + service: "rest", + config: { databaseUrl: "postgresql://localhost/postgres", jwtSecret: "active-hmac-secret" }, + }); + const endpoints: ReadonlyMap = new Map([ + ["http", { kind: "tcp" as const, host: "127.0.0.1", port: 54321 }], + ["admin", { kind: "tcp" as const, host: "127.0.0.1", port: 54322 }], + ]); + const env = yield* spec.env(creation, endpoints, false); + + expect(spec.healthPort).toBe("admin"); + expect(spec.healthPath).toBe("/ready"); + expect(env.PGRST_SERVER_PORT).toBe("54321"); + expect(env.PGRST_ADMIN_SERVER_PORT).toBe("54322"); + }), +); + it("forwards Realtime JWKS while retaining its default JWT secret", () => { const creation = Schema.decodeSync(Realtime.Creation)({ service: "realtime",