Skip to content
1 change: 1 addition & 0 deletions internal/db/start/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion internal/functions/deploy/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
27 changes: 13 additions & 14 deletions internal/link/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
48 changes: 32 additions & 16 deletions internal/link/link_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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"))
Expand All @@ -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
Expand Down Expand Up @@ -372,59 +366,81 @@ 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).
ReplyError(pgerrcode.InsufficientPrivilege, "permission denied for relation supabase_migrations").
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)
})
}
13 changes: 2 additions & 11 deletions internal/start/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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",
},
Expand Down
7 changes: 0 additions & 7 deletions internal/start/templates/kong.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
9 changes: 6 additions & 3 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
1 change: 1 addition & 0 deletions pkg/config/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
6 changes: 3 additions & 3 deletions pkg/config/templates/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down