diff --git a/internal/db/start/start.go b/internal/db/start/start.go index 608e5ca61f..bb3a97d67a 100644 --- a/internal/db/start/start.go +++ b/internal/db/start/start.go @@ -305,6 +305,7 @@ func initStorageJob(host string) utils.DockerJob { Image: utils.Config.Storage.Image, Env: []string{ "DB_INSTALL_ROLES=false", + "DB_MIGRATIONS_FREEZE_AT=" + utils.Config.Storage.TargetMigration, "ANON_KEY=" + utils.Config.Auth.AnonKey.Value, "SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey.Value, "PGRST_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value, diff --git a/internal/functions/deploy/bundle.go b/internal/functions/deploy/bundle.go index fa47b22fbf..974838374e 100644 --- a/internal/functions/deploy/bundle.go +++ b/internal/functions/deploy/bundle.go @@ -50,7 +50,7 @@ func (b *dockerBundler) Bundle(ctx context.Context, slug, entrypoint, importMap } hostOutputPath := filepath.Join(hostOutputDir, "output.eszip") // Create exec command - cmd := []string{"bundle", "--entrypoint", utils.ToDockerPath(entrypoint), "--output", utils.ToDockerPath(hostOutputPath)} + cmd := []string{"bundle", "--entrypoint", utils.ToDockerPath(entrypoint), "--output", utils.ToDockerPath(hostOutputPath), "--decorator", "tc39"} if len(importMap) > 0 { cmd = append(cmd, "--import-map", utils.ToDockerPath(importMap)) } diff --git a/internal/link/link.go b/internal/link/link.go index d35321e2ea..17d9306057 100644 --- a/internal/link/link.go +++ b/internal/link/link.go @@ -45,7 +45,7 @@ func Run(ctx context.Context, projectRef string, fsys afero.Fs, options ...func( // 2. Check database connection config := flags.GetDbConfigOptionalPassword(projectRef) if len(config.Password) > 0 { - if err := linkDatabase(ctx, config, options...); err != nil { + if err := linkDatabase(ctx, config, fsys, options...); err != nil { return err } // Save database password @@ -76,7 +76,7 @@ func Run(ctx context.Context, projectRef string, fsys afero.Fs, options ...func( func LinkServices(ctx context.Context, projectRef, anonKey string, fsys afero.Fs) { // Ignore non-fatal errors linking services var wg sync.WaitGroup - wg.Add(8) + wg.Add(7) go func() { defer wg.Done() if err := linkDatabaseSettings(ctx, projectRef); err != nil && viper.GetBool("DEBUG") { @@ -120,12 +120,6 @@ func LinkServices(ctx context.Context, projectRef, anonKey string, fsys afero.Fs fmt.Fprintln(os.Stderr, err) } }() - go func() { - defer wg.Done() - if err := linkStorageVersion(ctx, api, fsys); err != nil && viper.GetBool("DEBUG") { - fmt.Fprintln(os.Stderr, err) - } - }() wg.Wait() } @@ -178,12 +172,14 @@ func linkStorage(ctx context.Context, projectRef string) error { return nil } -func linkStorageVersion(ctx context.Context, api tenant.TenantAPI, fsys afero.Fs) error { - version, err := api.GetStorageVersion(ctx) - if err != nil { - return err +const GET_LATEST_STORAGE_MIGRATION = "SELECT name FROM storage.migrations ORDER BY id DESC LIMIT 1" + +func linkStorageVersion(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error { + var name string + if err := conn.QueryRow(ctx, GET_LATEST_STORAGE_MIGRATION).Scan(&name); err != nil { + return errors.Errorf("failed to fetch storage migration: %w", err) } - return utils.WriteFile(utils.StorageVersionPath, []byte(version), fsys) + return utils.WriteFile(utils.StorageVersionPath, []byte(name), fsys) } func linkDatabaseSettings(ctx context.Context, projectRef string) error { @@ -197,13 +193,16 @@ func linkDatabaseSettings(ctx context.Context, projectRef string) error { return nil } -func linkDatabase(ctx context.Context, config pgconn.Config, options ...func(*pgx.ConnConfig)) error { +func linkDatabase(ctx context.Context, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { conn, err := utils.ConnectByConfig(ctx, config, options...) if err != nil { return err } defer conn.Close(context.Background()) updatePostgresConfig(conn) + if err := linkStorageVersion(ctx, conn, fsys); err != nil { + fmt.Fprintln(os.Stderr, err) + } // If `schema_migrations` doesn't exist on the remote database, create it. if err := migration.CreateMigrationTable(ctx, conn); err != nil { return err diff --git a/internal/link/link_test.go b/internal/link/link_test.go index 18c090ad3e..03c33f4bc3 100644 --- a/internal/link/link_test.go +++ b/internal/link/link_test.go @@ -46,6 +46,8 @@ func TestLinkCommand(t *testing.T) { // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) + conn.Query(GET_LATEST_STORAGE_MIGRATION). + Reply("SELECT 1", []interface{}{"custom-metadata"}) helper.MockMigrationHistory(conn) helper.MockSeedHistory(conn) // Flush pending mocks after test execution @@ -98,10 +100,6 @@ func TestLinkCommand(t *testing.T) { Get("/rest/v1/"). Reply(200). JSON(rest) - gock.New("https://" + utils.GetSupabaseHost(project)). - Get("/storage/v1/version"). - Reply(200). - BodyString("0.40.4") // Run test err := Run(context.Background(), project, fsys, conn.Intercept) // Check error @@ -163,9 +161,6 @@ func TestLinkCommand(t *testing.T) { gock.New("https://" + utils.GetSupabaseHost(project)). Get("/rest/v1/"). ReplyError(errors.New("network error")) - gock.New("https://" + utils.GetSupabaseHost(project)). - Get("/storage/v1/version"). - ReplyError(errors.New("network error")) // Run test err := Run(context.Background(), project, fsys, func(cc *pgx.ConnConfig) { cc.LookupFunc = func(ctx context.Context, host string) (addrs []string, err error) { @@ -217,9 +212,6 @@ func TestLinkCommand(t *testing.T) { gock.New("https://" + utils.GetSupabaseHost(project)). Get("/rest/v1/"). ReplyError(errors.New("network error")) - gock.New("https://" + utils.GetSupabaseHost(project)). - Get("/storage/v1/version"). - ReplyError(errors.New("network error")) gock.New(utils.DefaultApiHost). Get("/v1/projects"). ReplyError(errors.New("network error")) @@ -237,6 +229,8 @@ func TestLinkCommand(t *testing.T) { func TestStatusCheck(t *testing.T) { project := "test-project" + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) t.Run("updates postgres version when healthy", func(t *testing.T) { // Setup in-memory fs @@ -372,50 +366,69 @@ func TestLinkPostgrest(t *testing.T) { func TestLinkDatabase(t *testing.T) { t.Run("throws error on connect failure", func(t *testing.T) { + // Setup in-memory fs + fsys := afero.NewMemMapFs() // Run test - err := linkDatabase(context.Background(), pgconn.Config{}) + err := linkDatabase(context.Background(), pgconn.Config{}, fsys) // Check error assert.ErrorContains(t, err, "invalid port (outside range)") }) t.Run("ignores missing server version", func(t *testing.T) { + // Setup in-memory fs + fsys := afero.NewMemMapFs() // Setup mock postgres conn := pgtest.NewWithStatus(map[string]string{ "standard_conforming_strings": "on", }) defer conn.Close(t) + conn.Query(GET_LATEST_STORAGE_MIGRATION). + Reply("SELECT 1", []interface{}{"custom-metadata"}) helper.MockMigrationHistory(conn) helper.MockSeedHistory(conn) // Run test - err := linkDatabase(context.Background(), dbConfig, conn.Intercept) + err := linkDatabase(context.Background(), dbConfig, fsys, conn.Intercept) // Check error assert.NoError(t, err) + version, err := afero.ReadFile(fsys, utils.StorageVersionPath) + assert.NoError(t, err) + assert.Equal(t, "custom-metadata", string(version)) }) t.Run("updates config to newer db version", func(t *testing.T) { utils.Config.Db.MajorVersion = 14 + // Setup in-memory fs + fsys := afero.NewMemMapFs() // Setup mock postgres conn := pgtest.NewWithStatus(map[string]string{ "standard_conforming_strings": "on", "server_version": "15.0", }) defer conn.Close(t) + conn.Query(GET_LATEST_STORAGE_MIGRATION). + Reply("SELECT 1", []interface{}{"custom-metadata"}) helper.MockMigrationHistory(conn) helper.MockSeedHistory(conn) // Run test - err := linkDatabase(context.Background(), dbConfig, conn.Intercept) + err := linkDatabase(context.Background(), dbConfig, fsys, conn.Intercept) // Check error assert.NoError(t, err) - utils.Config.Db.MajorVersion = 15 assert.Equal(t, uint(15), utils.Config.Db.MajorVersion) + version, err := afero.ReadFile(fsys, utils.StorageVersionPath) + assert.NoError(t, err) + assert.Equal(t, "custom-metadata", string(version)) }) t.Run("throws error on query failure", func(t *testing.T) { utils.Config.Db.MajorVersion = 14 + // Setup in-memory fs + fsys := afero.NewMemMapFs() // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) - conn.Query(migration.SET_LOCK_TIMEOUT). + conn.Query(GET_LATEST_STORAGE_MIGRATION). + ReplyError(pgerrcode.InsufficientPrivilege, "permission denied for relation migrations"). + Query(migration.SET_LOCK_TIMEOUT). Query(migration.CREATE_VERSION_SCHEMA). Reply("CREATE SCHEMA"). Query(migration.CREATE_VERSION_TABLE). @@ -423,8 +436,11 @@ func TestLinkDatabase(t *testing.T) { Query(migration.ADD_STATEMENTS_COLUMN). Query(migration.ADD_NAME_COLUMN) // Run test - err := linkDatabase(context.Background(), dbConfig, conn.Intercept) + err := linkDatabase(context.Background(), dbConfig, fsys, conn.Intercept) // Check error assert.ErrorContains(t, err, "ERROR: permission denied for relation supabase_migrations (SQLSTATE 42501)") + exists, err := afero.Exists(fsys, utils.StorageVersionPath) + assert.NoError(t, err) + assert.False(t, exists) }) } diff --git a/internal/start/start.go b/internal/start/start.go index 4f31306b76..b13f88965d 100644 --- a/internal/start/start.go +++ b/internal/start/start.go @@ -31,7 +31,6 @@ import ( "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" "github.com/supabase/cli/pkg/config" - "golang.org/x/mod/semver" ) func Run(ctx context.Context, fsys afero.Fs, excludedContainers []string, ignoreHealthCheck bool) error { @@ -90,18 +89,10 @@ type kongConfig struct { ApiPort uint16 } -// TODO: deprecate after removing storage headers from kong -func StorageVersionBelow(target string) bool { - parts := strings.Split(utils.Config.Storage.Image, ":v") - return semver.Compare(parts[len(parts)-1], target) < 0 -} - var ( //go:embed templates/kong.yml kongConfigEmbed string - kongConfigTemplate = template.Must(template.New("kongConfig").Funcs(template.FuncMap{ - "StorageVersionBelow": StorageVersionBelow, - }).Parse(kongConfigEmbed)) + kongConfigTemplate = template.Must(template.New("kongConfig").Parse(kongConfigEmbed)) //go:embed templates/custom_nginx.template nginxConfigEmbed string @@ -838,6 +829,7 @@ EOF container.Config{ Image: utils.Config.Storage.Image, Env: []string{ + "DB_MIGRATIONS_FREEZE_AT=" + utils.Config.Storage.TargetMigration, "ANON_KEY=" + utils.Config.Auth.AnonKey.Value, "SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey.Value, "AUTH_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value, @@ -856,7 +848,6 @@ EOF "S3_PROTOCOL_ACCESS_KEY_ID=" + utils.Config.Storage.S3Credentials.AccessKeyId, "S3_PROTOCOL_ACCESS_KEY_SECRET=" + utils.Config.Storage.S3Credentials.SecretAccessKey, "S3_PROTOCOL_PREFIX=/storage/v1", - fmt.Sprintf("S3_ALLOW_FORWARDED_HEADER=%v", StorageVersionBelow("1.10.1")), "UPLOAD_FILE_SIZE_LIMIT=52428800000", "UPLOAD_FILE_SIZE_LIMIT_STANDARD=5242880000", }, diff --git a/internal/start/templates/kong.yml b/internal/start/templates/kong.yml index 8f1d28fd81..71f0453447 100644 --- a/internal/start/templates/kong.yml +++ b/internal/start/templates/kong.yml @@ -119,13 +119,6 @@ services: - /storage/v1/ plugins: - name: cors -{{if StorageVersionBelow "1.10.1" }} - - name: request-transformer - config: - add: - headers: - - "Forwarded: host={{ .ApiHost }}:{{ .ApiPort }};proto=http" -{{end}} - name: pg-meta _comment: "pg-meta: /pg/* -> http://pg-meta:8080/*" url: http://{{ .PgmetaId }}:8080/ diff --git a/pkg/config/config.go b/pkg/config/config.go index 5471ca4e49..7fcf1cf887 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -615,13 +615,16 @@ func (c *config) Load(path string, fsys fs.FS) error { if version, err := fs.ReadFile(fsys, builder.RestVersionPath); err == nil && len(version) > 0 { c.Api.Image = replaceImageTag(Images.Postgrest, string(version)) } - if version, err := fs.ReadFile(fsys, builder.StorageVersionPath); err == nil && len(version) > 0 { - c.Storage.Image = replaceImageTag(Images.Storage, string(version)) - } if version, err := fs.ReadFile(fsys, builder.GotrueVersionPath); err == nil && len(version) > 0 { c.Auth.Image = replaceImageTag(Images.Gotrue, string(version)) } } + if version, err := fs.ReadFile(fsys, builder.StorageVersionPath); err == nil && len(version) > 0 { + // For backwards compatibility, exclude all strings that look like semver + if v := strings.TrimSpace(string(version)); !semver.IsValid(v) { + c.Storage.TargetMigration = v + } + } if version, err := fs.ReadFile(fsys, builder.EdgeRuntimeVersionPath); err == nil && len(version) > 0 { c.EdgeRuntime.Image = replaceImageTag(Images.EdgeRuntime, string(version)) } diff --git a/pkg/config/storage.go b/pkg/config/storage.go index 9b82ac6e92..fdb3b27ff3 100644 --- a/pkg/config/storage.go +++ b/pkg/config/storage.go @@ -10,6 +10,7 @@ type ( storage struct { Enabled bool `toml:"enabled"` Image string `toml:"-"` + TargetMigration string `toml:"-"` ImgProxyImage string `toml:"-"` FileSizeLimit sizeInBytes `toml:"file_size_limit"` ImageTransformation *imageTransformation `toml:"image_transformation"` diff --git a/pkg/config/templates/Dockerfile b/pkg/config/templates/Dockerfile index bcee3a0bc1..15e2e96a2c 100644 --- a/pkg/config/templates/Dockerfile +++ b/pkg/config/templates/Dockerfile @@ -1,9 +1,9 @@ # Exposed for updates by .github/dependabot.yml -FROM supabase/postgres:17.4.1.017 AS pg +FROM supabase/postgres:17.4.1.019 AS pg # Append to ServiceImages when adding new dependencies below FROM library/kong:2.8.1 AS kong FROM axllent/mailpit:v1.22.3 AS mailpit -FROM postgrest/postgrest:v12.2.10 AS postgrest +FROM postgrest/postgrest:v12.2.11 AS postgrest FROM supabase/postgres-meta:v0.88.9 AS pgmeta FROM supabase/studio:2025.04.21-sha-173cc56 AS studio FROM darthsim/imgproxy:v3.8.0 AS imgproxy @@ -12,7 +12,7 @@ FROM timberio/vector:0.28.1-alpine AS vector FROM supabase/supavisor:2.5.1 AS supavisor FROM supabase/gotrue:v2.171.0 AS gotrue FROM supabase/realtime:v2.34.47 AS realtime -FROM supabase/storage-api:v1.22.3 AS storage +FROM supabase/storage-api:v1.22.7 AS storage FROM supabase/logflare:1.12.0 AS logflare # Append to JobImages when adding new dependencies below FROM supabase/pgadmin-schema-diff:cli-0.0.5 AS differ