diff --git a/apps/cli-go/internal/start/start.go b/apps/cli-go/internal/start/start.go index 881f8dab73..6ce6a4434d 100644 --- a/apps/cli-go/internal/start/start.go +++ b/apps/cli-go/internal/start/start.go @@ -1107,29 +1107,7 @@ EOF ctx, container.Config{ Image: utils.Config.Studio.Image, - Env: []string{ - "CURRENT_CLI_VERSION=" + utils.Version, - "STUDIO_PG_META_URL=http://" + utils.PgmetaId + ":8080", - "POSTGRES_PASSWORD=" + dbConfig.Password, - "SUPABASE_URL=http://" + utils.KongId + ":8000", - "SUPABASE_PUBLIC_URL=" + utils.Config.Studio.ApiUrl, - "AUTH_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value, - "SUPABASE_ANON_KEY=" + utils.Config.Auth.AnonKey.Value, - "SUPABASE_SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey.Value, - "LOGFLARE_PRIVATE_ACCESS_TOKEN=" + utils.Config.Analytics.ApiKey, - "OPENAI_API_KEY=" + utils.Config.Studio.OpenaiApiKey.Value, - "PGRST_DB_SCHEMAS=" + strings.Join(utils.Config.Api.Schemas, ","), - "PGRST_DB_EXTRA_SEARCH_PATH=" + strings.Join(utils.Config.Api.ExtraSearchPath, ","), - fmt.Sprintf("PGRST_DB_MAX_ROWS=%d", utils.Config.Api.MaxRows), - fmt.Sprintf("LOGFLARE_URL=http://%v:4000", utils.LogflareId), - fmt.Sprintf("NEXT_PUBLIC_ENABLE_LOGS=%v", utils.Config.Analytics.Enabled), - fmt.Sprintf("NEXT_ANALYTICS_BACKEND_PROVIDER=%v", utils.Config.Analytics.Backend), - "EDGE_FUNCTIONS_MANAGEMENT_FOLDER=" + utils.ToDockerPath(filepath.Join(workdir, utils.FunctionsDir)), - "SNIPPETS_MANAGEMENT_FOLDER=" + containerSnippetsPath, - // Ref: https://github.com/vercel/next.js/issues/51684#issuecomment-1612834913 - "HOSTNAME=0.0.0.0", - "POSTGRES_USER_READ_WRITE=postgres", - }, + Env: buildStudioEnv(dbConfig, workdir, containerSnippetsPath), Healthcheck: &container.HealthConfig{ Test: []string{"CMD-SHELL", `node --eval="fetch('http://127.0.0.1:3000/api/platform/profile').then((r) => {if (!r.ok) throw new Error(r.status)})"`}, Interval: 10 * time.Second, @@ -1280,6 +1258,36 @@ func formatMapForEnvConfig(input map[string]string, output *bytes.Buffer) { } } +func buildStudioEnv(dbConfig pgconn.Config, workdir, containerSnippetsPath string) []string { + return []string{ + "CURRENT_CLI_VERSION=" + utils.Version, + "STUDIO_PG_META_URL=http://" + utils.PgmetaId + ":8080", + "POSTGRES_PASSWORD=" + dbConfig.Password, + "SUPABASE_URL=http://" + utils.KongId + ":8000", + "SUPABASE_PUBLIC_URL=" + utils.Config.Studio.ApiUrl, + "AUTH_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value, + "SUPABASE_ANON_KEY=" + utils.Config.Auth.AnonKey.Value, + "SUPABASE_SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey.Value, + "SUPABASE_PUBLISHABLE_KEY=" + utils.Config.Auth.PublishableKey.Value, + "SUPABASE_SECRET_KEY=" + utils.Config.Auth.SecretKey.Value, + "S3_PROTOCOL_ACCESS_KEY_ID=" + utils.Config.Storage.S3Credentials.AccessKeyId, + "S3_PROTOCOL_ACCESS_KEY_SECRET=" + utils.Config.Storage.S3Credentials.SecretAccessKey, + "LOGFLARE_PRIVATE_ACCESS_TOKEN=" + utils.Config.Analytics.ApiKey, + "OPENAI_API_KEY=" + utils.Config.Studio.OpenaiApiKey.Value, + "PGRST_DB_SCHEMAS=" + strings.Join(utils.Config.Api.Schemas, ","), + "PGRST_DB_EXTRA_SEARCH_PATH=" + strings.Join(utils.Config.Api.ExtraSearchPath, ","), + fmt.Sprintf("PGRST_DB_MAX_ROWS=%d", utils.Config.Api.MaxRows), + fmt.Sprintf("LOGFLARE_URL=http://%v:4000", utils.LogflareId), + fmt.Sprintf("NEXT_PUBLIC_ENABLE_LOGS=%v", utils.Config.Analytics.Enabled), + fmt.Sprintf("NEXT_ANALYTICS_BACKEND_PROVIDER=%v", utils.Config.Analytics.Backend), + "EDGE_FUNCTIONS_MANAGEMENT_FOLDER=" + utils.ToDockerPath(filepath.Join(workdir, utils.FunctionsDir)), + "SNIPPETS_MANAGEMENT_FOLDER=" + containerSnippetsPath, + // Ref: https://github.com/vercel/next.js/issues/51684#issuecomment-1612834913 + "HOSTNAME=0.0.0.0", + "POSTGRES_USER_READ_WRITE=postgres", + } +} + func buildGotrueEnv(dbConfig pgconn.Config) []string { var testOTP bytes.Buffer if len(utils.Config.Auth.Sms.TestOTP) > 0 { diff --git a/apps/cli-go/internal/start/start_test.go b/apps/cli-go/internal/start/start_test.go index ee468f562c..afafbd815e 100644 --- a/apps/cli-go/internal/start/start_test.go +++ b/apps/cli-go/internal/start/start_test.go @@ -387,6 +387,50 @@ func TestBuildGotrueEnv(t *testing.T) { }) } +func TestBuildStudioEnv(t *testing.T) { + originalConfig := utils.Config + originalKongId := utils.KongId + originalPgmetaId := utils.PgmetaId + originalLogflareId := utils.LogflareId + originalVersion := utils.Version + t.Cleanup(func() { + utils.Config = originalConfig + utils.KongId = originalKongId + utils.PgmetaId = originalPgmetaId + utils.LogflareId = originalLogflareId + utils.Version = originalVersion + }) + + utils.Config = config.NewConfig() + utils.Config.Studio.ApiUrl = "http://127.0.0.1:54321" + utils.Config.Auth.JwtSecret.Value = "jwt-secret" + utils.Config.Auth.AnonKey.Value = "anon-key" + utils.Config.Auth.ServiceRoleKey.Value = "service-role-key" + utils.Config.Auth.PublishableKey.Value = "sb_publishable_test" + utils.Config.Auth.SecretKey.Value = "sb_secret_test" + utils.Config.Storage.S3Credentials.AccessKeyId = "s3-access-key" + utils.Config.Storage.S3Credentials.SecretAccessKey = "s3-secret-key" + utils.KongId = "test-kong" + utils.PgmetaId = "test-pgmeta" + utils.LogflareId = "test-logflare" + utils.Version = "test-version" + + env := envToMap(buildStudioEnv( + pgconn.Config{Password: "postgres"}, + "/project", + "/project/supabase/.temp/snippets", + )) + + assert.Equal(t, "anon-key", env["SUPABASE_ANON_KEY"]) + assert.Equal(t, "service-role-key", env["SUPABASE_SERVICE_KEY"]) + assert.Equal(t, "sb_publishable_test", env["SUPABASE_PUBLISHABLE_KEY"]) + assert.Equal(t, "sb_secret_test", env["SUPABASE_SECRET_KEY"]) + assert.Equal(t, "s3-access-key", env["S3_PROTOCOL_ACCESS_KEY_ID"]) + assert.Equal(t, "s3-secret-key", env["S3_PROTOCOL_ACCESS_KEY_SECRET"]) + assert.Equal(t, "http://test-kong:8000", env["SUPABASE_URL"]) + assert.Equal(t, "http://test-pgmeta:8080", env["STUDIO_PG_META_URL"]) +} + func TestFormatMapForEnvConfig(t *testing.T) { t.Run("It produces the correct format and removes the trailing comma", func(t *testing.T) { testcases := []struct { diff --git a/packages/stack/src/StackBuilder.ts b/packages/stack/src/StackBuilder.ts index 504f8d57d6..9975ca4fb0 100644 --- a/packages/stack/src/StackBuilder.ts +++ b/packages/stack/src/StackBuilder.ts @@ -27,7 +27,11 @@ import { makePostgresService, makePostgresServiceDocker } from "./services/postg import { makePostgrestService, makePostgrestServiceDocker } from "./services/postgrest.ts"; import { makeRealtimeServiceDocker } from "./services/realtime.ts"; import { type ServiceDependency } from "./services/service-utils.ts"; -import { makeStorageServiceDocker } from "./services/storage.ts"; +import { + LOCAL_S3_PROTOCOL_ACCESS_KEY_ID, + LOCAL_S3_PROTOCOL_ACCESS_KEY_SECRET, + makeStorageServiceDocker, +} from "./services/storage.ts"; import { makeStudioServiceDocker } from "./services/studio.ts"; import { makeVectorServiceDocker } from "./services/vector.ts"; import type { PreparedStackArtifacts } from "./StackPreparation.ts"; @@ -879,6 +883,8 @@ export class StackBuilder extends Context.Service< pgmetaUrl: pgmetaConfig === false ? "" : `http://${serviceHost}:${pgmetaConfig.port}`, publishableKey: config.publishableKey, secretKey: config.secretKey, + s3ProtocolAccessKeyId: LOCAL_S3_PROTOCOL_ACCESS_KEY_ID, + s3ProtocolAccessKeySecret: LOCAL_S3_PROTOCOL_ACCESS_KEY_SECRET, jwtSecret: config.jwtSecret, analyticsEnabled: config.analytics !== false, analyticsBackend: config.analytics !== false ? config.analytics.backend : "postgres", diff --git a/packages/stack/src/services/services.unit.test.ts b/packages/stack/src/services/services.unit.test.ts index 6ddb1354c7..2db96bb9af 100644 --- a/packages/stack/src/services/services.unit.test.ts +++ b/packages/stack/src/services/services.unit.test.ts @@ -14,6 +14,8 @@ import { import { makePostgresService, makePostgresServiceDocker } from "./postgres.ts"; import { makePostgrestService } from "./postgrest.ts"; import { makePoolerServiceDocker, poolerContainerPorts } from "./pooler.ts"; +import { LOCAL_S3_PROTOCOL_ACCESS_KEY_ID, LOCAL_S3_PROTOCOL_ACCESS_KEY_SECRET } from "./storage.ts"; +import { makeStudioServiceDocker } from "./studio.ts"; import { makeVectorServiceDocker } from "./vector.ts"; import { DEFAULT_VERSIONS, dockerImageForService } from "../versions.ts"; @@ -81,6 +83,39 @@ describe("analyticsDockerRuntimeNetwork", () => { }); }); +describe("makeStudioServiceDocker", () => { + it("injects legacy keys, opaque keys, and S3 protocol credentials", () => { + const def = makeStudioServiceDocker({ + image: dockerImageForService("studio", DEFAULT_VERSIONS.studio), + apiPort: API_PORT, + port: 54323, + apiUrl: "http://host.docker.internal:54321", + publicApiUrl: "http://127.0.0.1:54321", + pgmetaUrl: "http://host.docker.internal:54322", + publishableKey: "sb_publishable_test", + secretKey: "sb_secret_test", + s3ProtocolAccessKeyId: LOCAL_S3_PROTOCOL_ACCESS_KEY_ID, + s3ProtocolAccessKeySecret: LOCAL_S3_PROTOCOL_ACCESS_KEY_SECRET, + jwtSecret: JWT_SECRET, + analyticsEnabled: true, + analyticsBackend: "postgres", + analyticsUrl: "http://host.docker.internal:54327", + analyticsApiKey: "test-api-key", + networkArgs: ["-p", "54323:54323"], + dependencies: [{ service: "pgmeta", condition: "healthy" }], + }); + + expect(def.args).toContain("SUPABASE_ANON_KEY=sb_publishable_test"); + expect(def.args).toContain("SUPABASE_SERVICE_KEY=sb_secret_test"); + expect(def.args).toContain("SUPABASE_PUBLISHABLE_KEY=sb_publishable_test"); + expect(def.args).toContain("SUPABASE_SECRET_KEY=sb_secret_test"); + expect(def.args).toContain(`S3_PROTOCOL_ACCESS_KEY_ID=${LOCAL_S3_PROTOCOL_ACCESS_KEY_ID}`); + expect(def.args).toContain( + `S3_PROTOCOL_ACCESS_KEY_SECRET=${LOCAL_S3_PROTOCOL_ACCESS_KEY_SECRET}`, + ); + }); +}); + describe("makePostgresService (dockerAccessible)", () => { it("creates per-run pg_hba.conf instead of mutating shared cache", () => { const tempDir = mkdtempSync(path.join(tmpdir(), "stack-postgres-service-")); diff --git a/packages/stack/src/services/storage.ts b/packages/stack/src/services/storage.ts index beb4f3b88b..056a27605e 100644 --- a/packages/stack/src/services/storage.ts +++ b/packages/stack/src/services/storage.ts @@ -24,6 +24,9 @@ interface DockerStorageOptions { const STORAGE_DATA_DIR = "/var/lib/storage"; +export const LOCAL_S3_PROTOCOL_ACCESS_KEY_ID = "local"; +export const LOCAL_S3_PROTOCOL_ACCESS_KEY_SECRET = "local-secret"; + const orphanCleanup = (opts: DockerStorageOptions) => opts.cleanupDataDirOnExit ? removePathOnOrphanCleanup(opts.dataDir, { recursive: true }) : []; @@ -66,8 +69,8 @@ export const makeStorageServiceDocker = (opts: DockerStorageOptions): ServiceDef IMGPROXY_URL: opts.imgproxyUrl, TUS_URL_PATH: "/storage/v1/upload/resumable", S3_PROTOCOL_ENABLED: String(opts.s3ProtocolEnabled), - S3_PROTOCOL_ACCESS_KEY_ID: "local", - S3_PROTOCOL_ACCESS_KEY_SECRET: "local-secret", + S3_PROTOCOL_ACCESS_KEY_ID: LOCAL_S3_PROTOCOL_ACCESS_KEY_ID, + S3_PROTOCOL_ACCESS_KEY_SECRET: LOCAL_S3_PROTOCOL_ACCESS_KEY_SECRET, S3_PROTOCOL_PREFIX: "/storage/v1", UPLOAD_FILE_SIZE_LIMIT: "52428800000", UPLOAD_FILE_SIZE_LIMIT_STANDARD: "5242880000", diff --git a/packages/stack/src/services/studio.ts b/packages/stack/src/services/studio.ts index 18bc9275d4..7521fce3c7 100644 --- a/packages/stack/src/services/studio.ts +++ b/packages/stack/src/services/studio.ts @@ -10,6 +10,8 @@ interface DockerStudioOptions { readonly pgmetaUrl: string; readonly publishableKey: string; readonly secretKey: string; + readonly s3ProtocolAccessKeyId: string; + readonly s3ProtocolAccessKeySecret: string; readonly jwtSecret: string; readonly analyticsEnabled: boolean; readonly analyticsBackend: "postgres" | "bigquery"; @@ -48,6 +50,10 @@ export const makeStudioServiceDocker = (opts: DockerStudioOptions): ServiceDef = AUTH_JWT_SECRET: opts.jwtSecret, SUPABASE_ANON_KEY: opts.publishableKey, SUPABASE_SERVICE_KEY: opts.secretKey, + SUPABASE_PUBLISHABLE_KEY: opts.publishableKey, + SUPABASE_SECRET_KEY: opts.secretKey, + S3_PROTOCOL_ACCESS_KEY_ID: opts.s3ProtocolAccessKeyId, + S3_PROTOCOL_ACCESS_KEY_SECRET: opts.s3ProtocolAccessKeySecret, LOGFLARE_PRIVATE_ACCESS_TOKEN: opts.analyticsApiKey, LOGFLARE_URL: opts.analyticsUrl, NEXT_PUBLIC_ENABLE_LOGS: String(opts.analyticsEnabled),