diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index 8d3c6e967..02f4c3d8f 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -83,7 +83,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version: "16.x" registry-url: "https://registry.npmjs.org" diff --git a/.github/workflows/tag-npm.yml b/.github/workflows/tag-npm.yml index 0da2161f2..ac4a9ed1f 100644 --- a/.github/workflows/tag-npm.yml +++ b/.github/workflows/tag-npm.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version: "16.x" registry-url: "https://registry.npmjs.org" diff --git a/cmd/branches.go b/cmd/branches.go index 4faa54f67..3a3df328e 100644 --- a/cmd/branches.go +++ b/cmd/branches.go @@ -35,6 +35,7 @@ var ( } persistent bool withData bool + notifyURL string branchCreateCmd = &cobra.Command{ Use: "create [name]", @@ -59,6 +60,9 @@ var ( if cmdFlags.Changed("with-data") { body.WithData = &withData } + if cmdFlags.Changed("notify-url") { + body.NotifyUrl = ¬ifyURL + } return create.Run(cmd.Context(), body, afero.NewOsFs()) }, } @@ -124,6 +128,9 @@ var ( if cmdFlags.Changed("status") { body.Status = (*api.UpdateBranchBodyStatus)(&branchStatus.Value) } + if cmdFlags.Changed("notify-url") { + body.NotifyUrl = ¬ifyURL + } ctx := cmd.Context() fsys := afero.NewOsFs() if len(args) > 0 { @@ -203,6 +210,7 @@ func init() { createFlags.Var(&size, "size", "Select a desired instance size for the branch database.") createFlags.BoolVar(&persistent, "persistent", false, "Whether to create a persistent branch.") createFlags.BoolVar(&withData, "with-data", false, "Whether to clone production data to the branch database.") + createFlags.StringVar(¬ifyURL, "notify-url", "", "URL to notify when branch is active healthy.") branchesCmd.AddCommand(branchCreateCmd) branchesCmd.AddCommand(branchListCmd) branchesCmd.AddCommand(branchGetCmd) @@ -211,6 +219,7 @@ func init() { updateFlags.StringVar(&gitBranch, "git-branch", "", "Change the associated git branch.") updateFlags.BoolVar(&persistent, "persistent", false, "Switch between ephemeral and persistent branch.") updateFlags.Var(&branchStatus, "status", "Override the current branch status.") + updateFlags.StringVar(¬ifyURL, "notify-url", "", "URL to notify when branch is active healthy.") branchesCmd.AddCommand(branchUpdateCmd) branchesCmd.AddCommand(branchDeleteCmd) branchesCmd.AddCommand(branchDisableCmd) diff --git a/cmd/db.go b/cmd/db.go index cc8bca65b..9558dc274 100644 --- a/cmd/db.go +++ b/cmd/db.go @@ -271,7 +271,7 @@ func init() { dumpFlags := dbDumpCmd.Flags() dumpFlags.BoolVar(&dryRun, "dry-run", false, "Prints the pg_dump script that would be executed.") dumpFlags.BoolVar(&dataOnly, "data-only", false, "Dumps only data records.") - dumpFlags.BoolVar(&useCopy, "use-copy", false, "Uses copy statements in place of inserts.") + dumpFlags.BoolVar(&useCopy, "use-copy", false, "Use copy statements in place of inserts.") dumpFlags.StringSliceVarP(&excludeTable, "exclude", "x", []string{}, "List of schema.tables to exclude from data-only dump.") dumpFlags.BoolVar(&roleOnly, "role-only", false, "Dumps only cluster roles.") dbDumpCmd.MarkFlagsMutuallyExclusive("role-only", "data-only") diff --git a/cmd/link.go b/cmd/link.go index 9ed09fd86..f2422ff48 100644 --- a/cmd/link.go +++ b/cmd/link.go @@ -13,6 +13,8 @@ import ( ) var ( + skipPooler bool + linkCmd = &cobra.Command{ GroupID: groupLocalDev, Use: "link", @@ -35,7 +37,7 @@ var ( } // TODO: move this to root cmd cobra.CheckErr(viper.BindPFlag("DB_PASSWORD", cmd.Flags().Lookup("password"))) - return link.Run(ctx, flags.ProjectRef, fsys) + return link.Run(ctx, flags.ProjectRef, skipPooler, fsys) }, } ) @@ -44,6 +46,7 @@ func init() { linkFlags := linkCmd.Flags() linkFlags.StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.") linkFlags.StringVarP(&dbPassword, "password", "p", "", "Password to your remote Postgres database.") + linkFlags.BoolVar(&skipPooler, "skip-pooler", false, "Use direct connection instead of pooler.") // For some reason, BindPFlag only works for StringVarP instead of StringP rootCmd.AddCommand(linkCmd) } diff --git a/cmd/root.go b/cmd/root.go index 8329cd86c..5a1072284 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -260,7 +260,7 @@ func GetRootCmd() *cobra.Command { func addSentryScope(scope *sentry.Scope) { serviceImages := utils.Config.GetServiceImages() - imageToVersion := make(map[string]interface{}, len(serviceImages)) + imageToVersion := make(map[string]any, len(serviceImages)) for _, image := range serviceImages { parts := strings.Split(image, ":") // Bypasses sentry's IP sanitization rule, ie. 15.1.0.147 @@ -271,7 +271,7 @@ func addSentryScope(scope *sentry.Scope) { } } scope.SetContext("Services", imageToVersion) - scope.SetContext("Config", map[string]interface{}{ + scope.SetContext("Config", map[string]any{ "Image Registry": utils.GetRegistry(), "Project ID": flags.ProjectRef, }) diff --git a/cmd/start.go b/cmd/start.go index a7af80e0c..bd1cc1f8a 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "os" + "slices" "sort" "strings" @@ -18,7 +19,7 @@ func validateExcludedContainers(excludedContainers []string) { var invalidContainers []string for _, e := range excludedContainers { - if !utils.SliceContains(validContainers, e) { + if !slices.Contains(validContainers, e) { invalidContainers = append(invalidContainers, e) } } diff --git a/go.mod b/go.mod index 25ad32be3..6d5bfe49b 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/docker/docker v28.5.1+incompatible github.com/docker/go-connections v0.6.0 github.com/fsnotify/fsnotify v1.9.0 - github.com/getsentry/sentry-go v0.35.3 + github.com/getsentry/sentry-go v0.36.0 github.com/go-errors/errors v1.5.1 github.com/go-git/go-git/v5 v5.16.3 github.com/go-playground/validator/v10 v10.28.0 diff --git a/go.sum b/go.sum index ca58a3d3c..ad6b798e7 100644 --- a/go.sum +++ b/go.sum @@ -300,8 +300,8 @@ github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIp github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/getkin/kin-openapi v0.131.0 h1:NO2UeHnFKRYhZ8wg6Nyh5Cq7dHk4suQQr72a4pMrDxE= github.com/getkin/kin-openapi v0.131.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= -github.com/getsentry/sentry-go v0.35.3 h1:u5IJaEqZyPdWqe/hKlBKBBnMTSxB/HenCqF3QLabeds= -github.com/getsentry/sentry-go v0.35.3/go.mod h1:mdL49ixwT2yi57k5eh7mpnDyPybixPzlzEJFu0Z76QA= +github.com/getsentry/sentry-go v0.36.0 h1:UkCk0zV28PiGf+2YIONSSYiYhxwlERE5Li3JPpZqEns= +github.com/getsentry/sentry-go v0.36.0/go.mod h1:p5Im24mJBeruET8Q4bbcMfCQ+F+Iadc4L48tB1apo2c= github.com/ghostiam/protogetter v0.3.15 h1:1KF5sXel0HE48zh1/vn0Loiw25A9ApyseLzQuif1mLY= github.com/ghostiam/protogetter v0.3.15/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index b2986656a..3fd02fe65 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -99,7 +99,7 @@ func Run(ctx context.Context, starter StarterTemplate, fsys afero.Fs, options .. if err := flags.LoadConfig(fsys); err != nil { return err } - link.LinkServices(ctx, flags.ProjectRef, tenant.NewApiKey(keys).Anon, fsys) + link.LinkServices(ctx, flags.ProjectRef, tenant.NewApiKey(keys).Anon, false, fsys) if err := utils.WriteFile(utils.ProjectRefPath, []byte(flags.ProjectRef), fsys); err != nil { return err } diff --git a/internal/db/branch/switch_/switch__test.go b/internal/db/branch/switch_/switch__test.go index 3eca45cd1..f0e51d521 100644 --- a/internal/db/branch/switch_/switch__test.go +++ b/internal/db/branch/switch_/switch__test.go @@ -48,7 +48,7 @@ func TestSwitchCommand(t *testing.T) { Query(reset.TERMINATE_BACKENDS). Reply("SELECT 1"). Query(reset.COUNT_REPLICATION_SLOTS). - Reply("SELECT 1", []interface{}{0}). + Reply("SELECT 1", []any{0}). Query("ALTER DATABASE postgres RENAME TO main;"). Reply("ALTER DATABASE"). Query("ALTER DATABASE " + branch + " RENAME TO postgres;"). @@ -237,7 +237,7 @@ func TestSwitchDatabase(t *testing.T) { Query(reset.TERMINATE_BACKENDS). Reply("SELECT 1"). Query(reset.COUNT_REPLICATION_SLOTS). - Reply("SELECT 1", []interface{}{0}). + Reply("SELECT 1", []any{0}). Query("ALTER DATABASE postgres RENAME TO main;"). ReplyError(pgerrcode.DuplicateDatabase, `database "main" already exists`) // Setup mock docker @@ -267,7 +267,7 @@ func TestSwitchDatabase(t *testing.T) { Query(reset.TERMINATE_BACKENDS). Reply("SELECT 1"). Query(reset.COUNT_REPLICATION_SLOTS). - Reply("SELECT 1", []interface{}{0}). + Reply("SELECT 1", []any{0}). Query("ALTER DATABASE postgres RENAME TO main;"). Reply("ALTER DATABASE"). Query("ALTER DATABASE target RENAME TO postgres;"). diff --git a/internal/db/diff/diff_test.go b/internal/db/diff/diff_test.go index 213329e75..3c21a83cc 100644 --- a/internal/db/diff/diff_test.go +++ b/internal/db/diff/diff_test.go @@ -136,6 +136,8 @@ func TestMigrateShadow(t *testing.T) { Query(CREATE_TEMPLATE). Reply("CREATE DATABASE") helper.MockMigrationHistory(conn). + Query("RESET ALL"). + Reply("RESET"). Query(sql). Reply("CREATE SCHEMA"). Query(migration.INSERT_MIGRATION_VERSION, "0", "test", []string{sql}). @@ -313,6 +315,8 @@ create schema public`) Query(CREATE_TEMPLATE). Reply("CREATE DATABASE") helper.MockMigrationHistory(conn). + Query("RESET ALL"). + Reply("RESET"). Query(sql). Reply("CREATE SCHEMA"). Query(migration.INSERT_MIGRATION_VERSION, "0", "test", []string{sql}). diff --git a/internal/db/lint/lint_test.go b/internal/db/lint/lint_test.go index e8f4bc9a0..f43b2086d 100644 --- a/internal/db/lint/lint_test.go +++ b/internal/db/lint/lint_test.go @@ -62,7 +62,7 @@ func TestLintCommand(t *testing.T) { Query(ENABLE_PGSQL_CHECK). Reply("CREATE EXTENSION"). Query(checkSchemaScript, "public"). - Reply("SELECT 1", []interface{}{"f1", string(data)}). + Reply("SELECT 1", []any{"f1", string(data)}). Query("rollback").Reply("ROLLBACK") // Run test err = Run(context.Background(), []string{"public"}, "warning", "none", dbConfig, fsys, conn.Intercept) @@ -117,8 +117,8 @@ func TestLintDatabase(t *testing.T) { Reply("CREATE EXTENSION"). Query(checkSchemaScript, "public"). Reply("SELECT 2", - []interface{}{"f1", string(r1)}, - []interface{}{"f2", string(r2)}, + []any{"f1", string(r1)}, + []any{"f2", string(r2)}, ). Query("rollback").Reply("ROLLBACK") // Run test @@ -158,9 +158,9 @@ func TestLintDatabase(t *testing.T) { Query(ENABLE_PGSQL_CHECK). Reply("CREATE EXTENSION"). Query(checkSchemaScript, "public"). - Reply("SELECT 1", []interface{}{"where_clause", string(r1)}). + Reply("SELECT 1", []any{"where_clause", string(r1)}). Query(checkSchemaScript, "private"). - Reply("SELECT 1", []interface{}{"f2", string(r2)}). + Reply("SELECT 1", []any{"f2", string(r2)}). Query("rollback").Reply("ROLLBACK") // Run test result, err := LintDatabase(context.Background(), conn.MockClient(t), []string{"public", "private"}) @@ -191,7 +191,7 @@ func TestLintDatabase(t *testing.T) { Query(ENABLE_PGSQL_CHECK). Reply("CREATE EXTENSION"). Query(checkSchemaScript, "public"). - Reply("SELECT 1", []interface{}{"f1", "malformed"}). + Reply("SELECT 1", []any{"f1", "malformed"}). Query("rollback").Reply("ROLLBACK") // Run test _, err := LintDatabase(context.Background(), conn.MockClient(t), []string{"public"}) @@ -253,7 +253,7 @@ func TestPrintResult(t *testing.T) { Query(ENABLE_PGSQL_CHECK). Reply("CREATE EXTENSION"). Query(checkSchemaScript, "public"). - Reply("SELECT 1", []interface{}{"f1", `{"function":"22751","issues":[{"level":"warning","message":"test warning"}]}`}). + Reply("SELECT 1", []any{"f1", `{"function":"22751","issues":[{"level":"warning","message":"test warning"}]}`}). Query("rollback").Reply("ROLLBACK") // Run test err := Run(context.Background(), []string{"public"}, "warning", "warning", dbConfig, fsys, conn.Intercept) @@ -271,7 +271,7 @@ func TestPrintResult(t *testing.T) { Query(ENABLE_PGSQL_CHECK). Reply("CREATE EXTENSION"). Query(checkSchemaScript, "public"). - Reply("SELECT 1", []interface{}{"f1", `{"function":"22751","issues":[{"level":"error","message":"test error"}]}`}). + Reply("SELECT 1", []any{"f1", `{"function":"22751","issues":[{"level":"error","message":"test error"}]}`}). Query("rollback").Reply("ROLLBACK") // Run test err := Run(context.Background(), []string{"public"}, "warning", "error", dbConfig, fsys, conn.Intercept) @@ -289,7 +289,7 @@ func TestPrintResult(t *testing.T) { Query(ENABLE_PGSQL_CHECK). Reply("CREATE EXTENSION"). Query(checkSchemaScript, "public"). - Reply("SELECT 1", []interface{}{"f1", `{"function":"22751","issues":[{"level":"error","message":"test error"}]}`}). + Reply("SELECT 1", []any{"f1", `{"function":"22751","issues":[{"level":"error","message":"test error"}]}`}). Query("rollback").Reply("ROLLBACK") // Run test err := Run(context.Background(), []string{"public"}, "warning", "none", dbConfig, fsys, conn.Intercept) diff --git a/internal/db/pull/pull.go b/internal/db/pull/pull.go index feffe2ce2..9c822971b 100644 --- a/internal/db/pull/pull.go +++ b/internal/db/pull/pull.go @@ -7,6 +7,7 @@ import ( "math" "os" "path/filepath" + "slices" "strconv" "strings" @@ -120,7 +121,7 @@ func diffRemoteSchema(ctx context.Context, schema []string, path string, config func diffUserSchemas(ctx context.Context, schema []string, path string, config pgconn.Config, fsys afero.Fs) error { var managed, user []string for _, s := range schema { - if utils.SliceContains(managedSchemas, s) { + if slices.Contains(managedSchemas, s) { managed = append(managed, s) } else { user = append(user, s) diff --git a/internal/db/pull/pull_test.go b/internal/db/pull/pull_test.go index 8161f51e1..293448156 100644 --- a/internal/db/pull/pull_test.go +++ b/internal/db/pull/pull_test.go @@ -92,7 +92,7 @@ func TestPullSchema(t *testing.T) { conn := pgtest.NewConn() defer conn.Close(t) conn.Query(migration.LIST_MIGRATION_VERSION). - Reply("SELECT 1", []interface{}{"0"}). + Reply("SELECT 1", []any{"0"}). Query(migration.ListSchemas, migration.ManagedSchemas). ReplyError(pgerrcode.DuplicateTable, `relation "test" already exists`) // Run test @@ -116,7 +116,7 @@ func TestPullSchema(t *testing.T) { conn := pgtest.NewConn() defer conn.Close(t) conn.Query(migration.LIST_MIGRATION_VERSION). - Reply("SELECT 1", []interface{}{"0"}) + Reply("SELECT 1", []any{"0"}) // Run test err := run(context.Background(), []string{"public"}, "", conn.MockClient(t), fsys) // Check error @@ -167,7 +167,7 @@ func TestSyncRemote(t *testing.T) { conn := pgtest.NewConn() defer conn.Close(t) conn.Query(migration.LIST_MIGRATION_VERSION). - Reply("SELECT 1", []interface{}{"20220727064247"}) + Reply("SELECT 1", []any{"20220727064247"}) // Run test err := assertRemoteInSync(context.Background(), conn.MockClient(t), fsys) // Check error diff --git a/internal/db/push/push_test.go b/internal/db/push/push_test.go index 9bc1baf58..b5d0d2c7f 100644 --- a/internal/db/push/push_test.go +++ b/internal/db/push/push_test.go @@ -99,6 +99,8 @@ func TestMigrationPush(t *testing.T) { conn.Query(migration.LIST_MIGRATION_VERSION). Reply("SELECT 0") helper.MockMigrationHistory(conn). + Query("RESET ALL"). + Reply("RESET"). Query(migration.INSERT_MIGRATION_VERSION, "0", "test", nil). ReplyError(pgerrcode.NotNullViolation, `null value in column "version" of relation "schema_migrations"`) // Run test @@ -121,6 +123,8 @@ func TestPushAll(t *testing.T) { conn.Query(migration.LIST_MIGRATION_VERSION). Reply("SELECT 0") helper.MockMigrationHistory(conn). + Query("RESET ALL"). + Reply("RESET"). Query(migration.INSERT_MIGRATION_VERSION, "0", "test", nil). Reply("INSERT 0 1") // Run test @@ -179,6 +183,8 @@ func TestPushAll(t *testing.T) { Query(migration.SELECT_SEED_TABLE). Reply("SELECT 0") helper.MockMigrationHistory(conn). + Query("RESET ALL"). + Reply("RESET"). Query(migration.INSERT_MIGRATION_VERSION, "0", "test", nil). Reply("INSERT 0 1") helper.MockSeedHistory(conn). diff --git a/internal/db/reset/reset_test.go b/internal/db/reset/reset_test.go index 23454d070..9eeb0f150 100644 --- a/internal/db/reset/reset_test.go +++ b/internal/db/reset/reset_test.go @@ -206,7 +206,7 @@ func TestRecreateDatabase(t *testing.T) { Query(TERMINATE_BACKENDS). Reply("SELECT 1"). Query(COUNT_REPLICATION_SLOTS). - Reply("SELECT 1", []interface{}{0}). + Reply("SELECT 1", []any{0}). Query("DROP DATABASE IF EXISTS postgres WITH (FORCE)"). Reply("DROP DATABASE"). Query("CREATE DATABASE postgres WITH OWNER postgres"). @@ -269,7 +269,7 @@ func TestRecreateDatabase(t *testing.T) { Query(TERMINATE_BACKENDS). Reply("SELECT 1"). Query(COUNT_REPLICATION_SLOTS). - Reply("SELECT 1", []interface{}{0}). + Reply("SELECT 1", []any{0}). Query("DROP DATABASE IF EXISTS postgres WITH (FORCE)"). ReplyError(pgerrcode.ObjectInUse, `database "postgres" is used by an active logical replication slot`). Query("CREATE DATABASE postgres WITH OWNER postgres"). diff --git a/internal/hostnames/common_test.go b/internal/hostnames/common_test.go index e46e202ab..2c3755c7c 100644 --- a/internal/hostnames/common_test.go +++ b/internal/hostnames/common_test.go @@ -19,7 +19,7 @@ func TestVerifyCNAME(t *testing.T) { MatchParam("type", "5"). MatchHeader("accept", "application/dns-json"). Reply(http.StatusOK). - JSON(&map[string]interface{}{"Answer": []map[string]interface{}{ + JSON(&map[string]any{"Answer": []map[string]any{ { "Type": 5, "Data": "foobarbaz.supabase.co.", }, @@ -36,7 +36,7 @@ func TestVerifyCNAMEFailures(t *testing.T) { MatchParam("type", "5"). MatchHeader("accept", "application/dns-json"). Reply(http.StatusOK). - JSON(&map[string]interface{}{"Answer": []map[string]interface{}{ + JSON(&map[string]any{"Answer": []map[string]any{ { "Type": 28, "Data": "127.0.0.1", }, diff --git a/internal/init/init.go b/internal/init/init.go index 72241a0e0..4bb18581b 100644 --- a/internal/init/init.go +++ b/internal/init/init.go @@ -99,7 +99,7 @@ func updateGitIgnore(ignorePath string, fsys afero.Fs) error { return nil } -type VSCodeSettings map[string]interface{} +type VSCodeSettings map[string]any func loadUserSettings(path string, fsys afero.Fs) (VSCodeSettings, error) { data, err := afero.ReadFile(fsys, path) diff --git a/internal/link/link.go b/internal/link/link.go index 5d1f41aa3..d3e48fb22 100644 --- a/internal/link/link.go +++ b/internal/link/link.go @@ -6,13 +6,12 @@ import ( "net/http" "os" "strconv" - "sync" + "strings" "github.com/go-errors/errors" "github.com/jackc/pgconn" "github.com/jackc/pgx/v4" "github.com/spf13/afero" - "github.com/spf13/viper" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" "github.com/supabase/cli/internal/utils/tenant" @@ -20,9 +19,10 @@ import ( "github.com/supabase/cli/pkg/cast" cliConfig "github.com/supabase/cli/pkg/config" "github.com/supabase/cli/pkg/migration" + "github.com/supabase/cli/pkg/queue" ) -func Run(ctx context.Context, projectRef string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { +func Run(ctx context.Context, projectRef string, skipPooler bool, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { majorVersion := utils.Config.Db.MajorVersion if err := checkRemoteProjectStatus(ctx, projectRef, fsys); err != nil { return err @@ -33,7 +33,7 @@ func Run(ctx context.Context, projectRef string, fsys afero.Fs, options ...func( if err != nil { return err } - LinkServices(ctx, projectRef, keys.ServiceRole, fsys) + LinkServices(ctx, projectRef, keys.ServiceRole, skipPooler, fsys) // 2. Check database connection config := flags.NewDbConfigWithPassword(ctx, projectRef) @@ -58,60 +58,35 @@ major_version = %d return nil } -func LinkServices(ctx context.Context, projectRef, serviceKey string, fsys afero.Fs) { - // Ignore non-fatal errors linking services - var wg sync.WaitGroup - wg.Add(8) - go func() { - defer wg.Done() - if err := linkDatabaseSettings(ctx, projectRef); err != nil && viper.GetBool("DEBUG") { - fmt.Fprintln(os.Stderr, err) - } - }() - go func() { - defer wg.Done() - if err := linkNetworkRestrictions(ctx, projectRef); err != nil && viper.GetBool("DEBUG") { - fmt.Fprintln(os.Stderr, err) - } - }() - go func() { - defer wg.Done() - if err := linkPostgrest(ctx, projectRef); err != nil && viper.GetBool("DEBUG") { - fmt.Fprintln(os.Stderr, err) - } - }() - go func() { - defer wg.Done() - if err := linkGotrue(ctx, projectRef); err != nil && viper.GetBool("DEBUG") { - fmt.Fprintln(os.Stderr, err) - } - }() - go func() { - defer wg.Done() - if err := linkStorage(ctx, projectRef); err != nil && viper.GetBool("DEBUG") { - fmt.Fprintln(os.Stderr, err) - } - }() - go func() { - defer wg.Done() - if err := linkPooler(ctx, projectRef, fsys); err != nil && viper.GetBool("DEBUG") { - fmt.Fprintln(os.Stderr, err) - } - }() +func LinkServices(ctx context.Context, projectRef, serviceKey string, skipPooler bool, fsys afero.Fs) { + jq := queue.NewJobQueue(5) api := tenant.NewTenantAPI(ctx, projectRef, serviceKey) - go func() { - defer wg.Done() - if err := linkPostgrestVersion(ctx, api, fsys); err != nil && viper.GetBool("DEBUG") { - fmt.Fprintln(os.Stderr, err) - } - }() - go func() { - defer wg.Done() - if err := linkGotrueVersion(ctx, api, fsys); err != nil && viper.GetBool("DEBUG") { - fmt.Fprintln(os.Stderr, err) + jobs := []func() error{ + func() error { return linkDatabaseSettings(ctx, projectRef) }, + func() error { return linkNetworkRestrictions(ctx, projectRef) }, + func() error { return linkPostgrest(ctx, projectRef) }, + func() error { return linkGotrue(ctx, projectRef) }, + func() error { return linkStorage(ctx, projectRef) }, + func() error { + if skipPooler { + return fsys.RemoveAll(utils.PoolerUrlPath) + } + return linkPooler(ctx, projectRef, fsys) + }, + func() error { return linkPostgrestVersion(ctx, api, fsys) }, + func() error { return linkGotrueVersion(ctx, api, fsys) }, + func() error { return linkStorageVersion(ctx, api, fsys) }, + } + // Ignore non-fatal errors linking services + logger := utils.GetDebugLogger() + for _, job := range jobs { + if err := jq.Put(job); err != nil { + fmt.Fprintln(logger, err) } - }() - wg.Wait() + } + if err := jq.Collect(); err != nil { + fmt.Fprintln(logger, err) + } } func linkPostgrest(ctx context.Context, projectRef string) error { @@ -163,14 +138,22 @@ 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 + } + return utils.WriteFile(utils.StorageVersionPath, []byte(version), fsys) +} + 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 { +func linkStorageMigration(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(name), fsys) + return utils.WriteFile(utils.StorageMigrationPath, []byte(name), fsys) } func linkDatabaseSettings(ctx context.Context, projectRef string) error { @@ -202,7 +185,7 @@ func linkDatabase(ctx context.Context, config pgconn.Config, fsys afero.Fs, opti } defer conn.Close(context.Background()) updatePostgresConfig(conn) - if err := linkStorageVersion(ctx, conn, fsys); err != nil { + if err := linkStorageMigration(ctx, conn, fsys); err != nil { fmt.Fprintln(os.Stderr, err) } // If `schema_migrations` doesn't exist on the remote database, create it. @@ -239,8 +222,14 @@ func linkPooler(ctx context.Context, projectRef string, fsys afero.Fs) error { } func updatePoolerConfig(config api.SupavisorConfigResponse) { - utils.Config.Db.Pooler.ConnectionString = config.ConnectionString - utils.Config.Db.Pooler.PoolMode = cliConfig.PoolMode(config.PoolMode) + // Remove password from pooler connection string because the placeholder text + // [YOUR-PASSWORD] messes up pgconn.ParseConfig. The password must be percent + // escaped so we cannot simply call strings.Replace with actual password. + utils.Config.Db.Pooler.ConnectionString = strings.ReplaceAll(config.ConnectionString, ":[YOUR-PASSWORD]", "") + // Always use session mode for running migrations + if utils.Config.Db.Pooler.PoolMode = cliConfig.SessionMode; config.PoolMode != api.SupavisorConfigResponsePoolModeSession { + utils.Config.Db.Pooler.ConnectionString = strings.ReplaceAll(utils.Config.Db.Pooler.ConnectionString, ":6543/", ":5432/") + } if value, err := config.DefaultPoolSize.Get(); err == nil { utils.Config.Db.Pooler.DefaultPoolSize = cast.IntToUint(value) } diff --git a/internal/link/link_test.go b/internal/link/link_test.go index 686f12d46..c6a7339e0 100644 --- a/internal/link/link_test.go +++ b/internal/link/link_test.go @@ -50,7 +50,7 @@ func TestLinkCommand(t *testing.T) { conn.Query(utils.SET_SESSION_ROLE). Reply("SET ROLE"). Query(GET_LATEST_STORAGE_MIGRATION). - Reply("SELECT 1", []interface{}{"custom-metadata"}) + Reply("SELECT 1", []any{"custom-metadata"}) helper.MockMigrationHistory(conn) helper.MockSeedHistory(conn) // Flush pending mocks after test execution @@ -113,8 +113,13 @@ func TestLinkCommand(t *testing.T) { Get("/rest/v1/"). Reply(200). JSON(rest) + storage := "1.28.0" + gock.New("https://" + utils.GetSupabaseHost(project)). + Get("/storage/v1/version"). + Reply(200). + BodyString(storage) // Run test - err := Run(context.Background(), project, fsys, conn.Intercept) + err := Run(context.Background(), project, false, fsys, conn.Intercept) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -128,6 +133,9 @@ func TestLinkCommand(t *testing.T) { authVersion, err := afero.ReadFile(fsys, utils.GotrueVersionPath) assert.NoError(t, err) assert.Equal(t, []byte(auth.Version), authVersion) + storageVersion, err := afero.ReadFile(fsys, utils.StorageVersionPath) + assert.NoError(t, err) + assert.Equal(t, []byte("v"+storage), storageVersion) postgresVersion, err := afero.ReadFile(fsys, utils.PostgresVersionPath) assert.NoError(t, err) assert.Equal(t, []byte(mockPostgres.Database.Version), postgresVersion) @@ -180,8 +188,11 @@ 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) { + err := Run(context.Background(), project, false, fsys, func(cc *pgx.ConnConfig) { cc.LookupFunc = func(ctx context.Context, host string) (addrs []string, err error) { return nil, errors.New("hostname resolving error") } @@ -200,7 +211,7 @@ func TestLinkCommand(t *testing.T) { conn.Query(utils.SET_SESSION_ROLE). Reply("SET ROLE"). Query(GET_LATEST_STORAGE_MIGRATION). - Reply("SELECT 1", []interface{}{"custom-metadata"}) + Reply("SELECT 1", []any{"custom-metadata"}) helper.MockMigrationHistory(conn) helper.MockSeedHistory(conn) // Flush pending mocks after test execution @@ -251,11 +262,14 @@ 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")) // Run test - err := Run(context.Background(), project, fsys, conn.Intercept) + err := Run(context.Background(), project, false, fsys, conn.Intercept) // Check error assert.ErrorContains(t, err, "operation not permitted") assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -423,14 +437,14 @@ func TestLinkDatabase(t *testing.T) { }) defer conn.Close(t) conn.Query(GET_LATEST_STORAGE_MIGRATION). - Reply("SELECT 1", []interface{}{"custom-metadata"}) + Reply("SELECT 1", []any{"custom-metadata"}) helper.MockMigrationHistory(conn) helper.MockSeedHistory(conn) // Run test err := linkDatabase(context.Background(), dbConfig, fsys, conn.Intercept) // Check error assert.NoError(t, err) - version, err := afero.ReadFile(fsys, utils.StorageVersionPath) + version, err := afero.ReadFile(fsys, utils.StorageMigrationPath) assert.NoError(t, err) assert.Equal(t, "custom-metadata", string(version)) }) @@ -446,7 +460,7 @@ func TestLinkDatabase(t *testing.T) { }) defer conn.Close(t) conn.Query(GET_LATEST_STORAGE_MIGRATION). - Reply("SELECT 1", []interface{}{"custom-metadata"}) + Reply("SELECT 1", []any{"custom-metadata"}) helper.MockMigrationHistory(conn) helper.MockSeedHistory(conn) // Run test @@ -454,7 +468,7 @@ func TestLinkDatabase(t *testing.T) { // Check error assert.NoError(t, err) assert.Equal(t, uint(15), utils.Config.Db.MajorVersion) - version, err := afero.ReadFile(fsys, utils.StorageVersionPath) + version, err := afero.ReadFile(fsys, utils.StorageMigrationPath) assert.NoError(t, err) assert.Equal(t, "custom-metadata", string(version)) }) @@ -479,7 +493,7 @@ func TestLinkDatabase(t *testing.T) { 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) + exists, err := afero.Exists(fsys, utils.StorageMigrationPath) assert.NoError(t, err) assert.False(t, exists) }) diff --git a/internal/migration/apply/apply_test.go b/internal/migration/apply/apply_test.go index 286093743..f45891cfb 100644 --- a/internal/migration/apply/apply_test.go +++ b/internal/migration/apply/apply_test.go @@ -27,6 +27,8 @@ func TestMigrateDatabase(t *testing.T) { conn := pgtest.NewConn() defer conn.Close(t) helper.MockMigrationHistory(conn). + Query("RESET ALL"). + Reply("RESET"). Query(sql). Reply("CREATE SCHEMA"). Query(migration.INSERT_MIGRATION_VERSION, "0", "test", []string{sql}). @@ -50,6 +52,8 @@ func TestMigrateDatabase(t *testing.T) { conn := pgtest.NewConn() defer conn.Close(t) helper.MockMigrationHistory(conn). + Query("RESET ALL"). + Reply("RESET"). Query(sql). Reply("CREATE SCHEMA"). Query(migration.INSERT_MIGRATION_VERSION, "0", "test", []string{sql}). diff --git a/internal/migration/down/down_test.go b/internal/migration/down/down_test.go index 76baefe97..89d9ff66d 100644 --- a/internal/migration/down/down_test.go +++ b/internal/migration/down/down_test.go @@ -40,7 +40,7 @@ func TestMigrationsDown(t *testing.T) { conn := pgtest.NewConn() defer conn.Close(t) conn.Query(migration.LIST_MIGRATION_VERSION). - Reply("SELECT 2", []interface{}{"20221201000000"}, []interface{}{"20221201000001"}) + Reply("SELECT 2", []any{"20221201000000"}, []any{"20221201000001"}) // Run test err := Run(context.Background(), 1, dbConfig, fsys, conn.Intercept) // Check error @@ -54,7 +54,7 @@ func TestMigrationsDown(t *testing.T) { conn := pgtest.NewConn() defer conn.Close(t) conn.Query(migration.LIST_MIGRATION_VERSION). - Reply("SELECT 2", []interface{}{"20221201000000"}, []interface{}{"20221201000001"}) + Reply("SELECT 2", []any{"20221201000000"}, []any{"20221201000001"}) // Run test err := Run(context.Background(), 2, dbConfig, fsys, conn.Intercept) // Check error @@ -88,6 +88,8 @@ func TestResetRemote(t *testing.T) { conn.Query(migration.DropObjects). Reply("INSERT 0") helper.MockMigrationHistory(conn). + Query("RESET ALL"). + Reply("RESET"). Query(migration.INSERT_MIGRATION_VERSION, "0", "schema", nil). Reply("INSERT 0 1") // Run test @@ -110,6 +112,8 @@ func TestResetRemote(t *testing.T) { conn.Query(migration.DropObjects). Reply("INSERT 0") helper.MockMigrationHistory(conn). + Query("RESET ALL"). + Reply("RESET"). Query(migration.INSERT_MIGRATION_VERSION, "0", "schema", nil). Reply("INSERT 0 1") utils.Config.Db.Seed.Enabled = false diff --git a/internal/migration/list/list_test.go b/internal/migration/list/list_test.go index b23fa4a2d..9744174df 100644 --- a/internal/migration/list/list_test.go +++ b/internal/migration/list/list_test.go @@ -71,7 +71,7 @@ func TestRemoteMigrations(t *testing.T) { conn := pgtest.NewConn() defer conn.Close(t) conn.Query(migration.LIST_MIGRATION_VERSION). - Reply("SELECT 1", []interface{}{"20220727064247"}) + Reply("SELECT 1", []any{"20220727064247"}) // Run test versions, err := loadRemoteVersions(context.Background(), dbConfig, conn.Intercept) // Check error @@ -104,7 +104,7 @@ func TestRemoteMigrations(t *testing.T) { conn := pgtest.NewConn() defer conn.Close(t) conn.Query(migration.LIST_MIGRATION_VERSION). - Reply("SELECT 1", []interface{}{}) + Reply("SELECT 1", []any{}) // Run test _, err := loadRemoteVersions(context.Background(), dbConfig, conn.Intercept) // Check error diff --git a/internal/migration/squash/squash_test.go b/internal/migration/squash/squash_test.go index bda678b0b..15eb52c9f 100644 --- a/internal/migration/squash/squash_test.go +++ b/internal/migration/squash/squash_test.go @@ -85,10 +85,14 @@ func TestSquashCommand(t *testing.T) { conn := pgtest.NewConn() defer conn.Close(t) helper.MockMigrationHistory(conn). + Query("RESET ALL"). + Reply("RESET"). Query(sql). Reply("CREATE SCHEMA"). Query(migration.INSERT_MIGRATION_VERSION, "0", "init", []string{sql}). Reply("INSERT 0 1"). + Query("RESET ALL"). + Reply("RESET"). Query(migration.INSERT_MIGRATION_VERSION, "1", "target", nil). Reply("INSERT 0 1") // Run test @@ -308,6 +312,8 @@ func TestSquashMigrations(t *testing.T) { conn := pgtest.NewConn() defer conn.Close(t) helper.MockMigrationHistory(conn). + Query("RESET ALL"). + Reply("RESET"). Query(sql). Reply("CREATE SCHEMA"). Query(migration.INSERT_MIGRATION_VERSION, "0", "init", []string{sql}). diff --git a/internal/migration/up/up_test.go b/internal/migration/up/up_test.go index ea41680bc..85669d709 100644 --- a/internal/migration/up/up_test.go +++ b/internal/migration/up/up_test.go @@ -32,7 +32,7 @@ func TestPendingMigrations(t *testing.T) { conn := pgtest.NewConn() defer conn.Close(t) conn.Query(migration.LIST_MIGRATION_VERSION). - Reply("SELECT 2", []interface{}{"20221201000000"}, []interface{}{"20221201000001"}) + Reply("SELECT 2", []any{"20221201000000"}, []any{"20221201000001"}) // Run test pending, err := GetPendingMigrations(context.Background(), false, conn.MockClient(t), fsys) // Check error @@ -61,7 +61,7 @@ func TestPendingMigrations(t *testing.T) { conn := pgtest.NewConn() defer conn.Close(t) conn.Query(migration.LIST_MIGRATION_VERSION). - Reply("SELECT 1", []interface{}{"0"}) + Reply("SELECT 1", []any{"0"}) // Run test _, err := GetPendingMigrations(context.Background(), false, conn.MockClient(t), fsys) // Check error @@ -81,7 +81,7 @@ func TestPendingMigrations(t *testing.T) { conn := pgtest.NewConn() defer conn.Close(t) conn.Query(migration.LIST_MIGRATION_VERSION). - Reply("SELECT 1", []interface{}{"1"}) + Reply("SELECT 1", []any{"1"}) // Run test _, err := GetPendingMigrations(context.Background(), false, conn.MockClient(t), fsys) // Check error @@ -106,7 +106,7 @@ func TestIgnoreVersionMismatch(t *testing.T) { conn := pgtest.NewConn() defer conn.Close(t) conn.Query(migration.LIST_MIGRATION_VERSION). - Reply("SELECT 2", []interface{}{"20221201000000"}, []interface{}{"20221201000002"}) + Reply("SELECT 2", []any{"20221201000000"}, []any{"20221201000002"}) // Run test pending, err := GetPendingMigrations(context.Background(), true, conn.MockClient(t), fsys) // Check error @@ -130,7 +130,7 @@ func TestIgnoreVersionMismatch(t *testing.T) { conn := pgtest.NewConn() defer conn.Close(t) conn.Query(migration.LIST_MIGRATION_VERSION). - Reply("SELECT 2", []interface{}{"20221201000002"}, []interface{}{"20221201000004"}) + Reply("SELECT 2", []any{"20221201000002"}, []any{"20221201000004"}) // Run test _, err := GetPendingMigrations(context.Background(), true, conn.MockClient(t), fsys) // Check error @@ -153,11 +153,11 @@ func TestIgnoreVersionMismatch(t *testing.T) { defer conn.Close(t) conn.Query(migration.LIST_MIGRATION_VERSION). Reply("SELECT 5", - []interface{}{"20221201000000"}, - []interface{}{"20221201000001"}, - []interface{}{"20221201000002"}, - []interface{}{"20221201000003"}, - []interface{}{"20221201000004"}, + []any{"20221201000000"}, + []any{"20221201000001"}, + []any{"20221201000002"}, + []any{"20221201000003"}, + []any{"20221201000004"}, ) // Run test _, err := GetPendingMigrations(context.Background(), true, conn.MockClient(t), fsys) diff --git a/internal/postgresConfig/update/update.go b/internal/postgresConfig/update/update.go index f031b3378..4d7e42087 100644 --- a/internal/postgresConfig/update/update.go +++ b/internal/postgresConfig/update/update.go @@ -25,7 +25,7 @@ func Run(ctx context.Context, projectRef string, values []string, replaceOverrid newConfigOverrides[splits[0]] = splits[1] } // 2. If not in replace mode, retrieve current overrides - finalOverrides := make(map[string]interface{}) + finalOverrides := make(map[string]any) { if !replaceOverrides { config, err := get.GetCurrentPostgresConfig(ctx, projectRef) diff --git a/internal/sso/create/create.go b/internal/sso/create/create.go index 83cadd5fb..15dea6cc7 100644 --- a/internal/sso/create/create.go +++ b/internal/sso/create/create.go @@ -51,10 +51,10 @@ func Run(ctx context.Context, params RunParams) error { if params.AttributeMapping != "" { body.AttributeMapping = &struct { Keys map[string]struct { - Array *bool "json:\"array,omitempty\"" - Default *interface{} "json:\"default,omitempty\"" - Name *string "json:\"name,omitempty\"" - Names *[]string "json:\"names,omitempty\"" + Array *bool "json:\"array,omitempty\"" + Default *any "json:\"default,omitempty\"" + Name *string "json:\"name,omitempty\"" + Names *[]string "json:\"names,omitempty\"" } "json:\"keys\"" }{} if err := saml.ReadAttributeMappingFile(Fs, params.AttributeMapping, body.AttributeMapping); err != nil { diff --git a/internal/sso/internal/saml/files_test.go b/internal/sso/internal/saml/files_test.go index 320f2df13..cc5c59b85 100644 --- a/internal/sso/internal/saml/files_test.go +++ b/internal/sso/internal/saml/files_test.go @@ -33,10 +33,10 @@ func TestReadAttributeMappingFile(t *testing.T) { body := api.CreateProviderBody{ AttributeMapping: &struct { Keys map[string]struct { - Array *bool "json:\"array,omitempty\"" - Default *interface{} "json:\"default,omitempty\"" - Name *string "json:\"name,omitempty\"" - Names *[]string "json:\"names,omitempty\"" + Array *bool "json:\"array,omitempty\"" + Default *any "json:\"default,omitempty\"" + Name *string "json:\"name,omitempty\"" + Names *[]string "json:\"names,omitempty\"" } "json:\"keys\"" }{}, } diff --git a/internal/sso/update/update.go b/internal/sso/update/update.go index f92357c70..316a1360b 100644 --- a/internal/sso/update/update.go +++ b/internal/sso/update/update.go @@ -71,10 +71,10 @@ func Run(ctx context.Context, params RunParams) error { if params.AttributeMapping != "" { body.AttributeMapping = &struct { Keys map[string]struct { - Array *bool "json:\"array,omitempty\"" - Default *interface{} "json:\"default,omitempty\"" - Name *string "json:\"name,omitempty\"" - Names *[]string "json:\"names,omitempty\"" + Array *bool "json:\"array,omitempty\"" + Default *any "json:\"default,omitempty\"" + Name *string "json:\"name,omitempty\"" + Names *[]string "json:\"names,omitempty\"" } "json:\"keys\"" }{} if err := saml.ReadAttributeMappingFile(Fs, params.AttributeMapping, body.AttributeMapping); err != nil { diff --git a/internal/start/start.go b/internal/start/start.go index 1176eaa56..47f8b5c37 100644 --- a/internal/start/start.go +++ b/internal/start/start.go @@ -10,6 +10,7 @@ import ( "os" "path" "path/filepath" + "slices" "strconv" "strings" "text/template" @@ -1152,7 +1153,7 @@ EOF } fmt.Fprintln(os.Stderr, "Waiting for health checks...") - if utils.NoBackupVolume && utils.SliceContains(started, utils.StorageId) { + if utils.NoBackupVolume && slices.Contains(started, utils.StorageId) { if err := start.WaitForHealthyService(ctx, serviceTimeout, utils.StorageId); err != nil { return err } diff --git a/internal/start/start_test.go b/internal/start/start_test.go index 99df75378..5e4e578db 100644 --- a/internal/start/start_test.go +++ b/internal/start/start_test.go @@ -268,7 +268,7 @@ func TestFormatMapForEnvConfig(t *testing.T) { if len(output.Bytes()) > 0 { t.Error("No values should be expected when empty map is provided") } - for i := 0; i < 4; i++ { + for i := range 4 { output.Reset() input[keys[i]] = values[i] formatMapForEnvConfig(input, &output) diff --git a/internal/start/templates/kong.yml b/internal/start/templates/kong.yml index b1f4a64b6..0c098886d 100644 --- a/internal/start/templates/kong.yml +++ b/internal/start/templates/kong.yml @@ -41,7 +41,14 @@ services: - /auth/v1/ plugins: - name: cors - # TODO: validate apikey + - name: request-transformer + config: + add: + headers: + - "Authorization: {{ .BearerToken }}" + replace: + headers: + - "Authorization: {{ .BearerToken }}" - name: rest-v1 _comment: "PostgREST: /rest/v1/* -> http://rest:3000/*" url: http://{{ .RestId }}:3000/ diff --git a/internal/status/status.go b/internal/status/status.go index 4ed7afde2..ee087062b 100644 --- a/internal/status/status.go +++ b/internal/status/status.go @@ -11,6 +11,7 @@ import ( "net/url" "os" "reflect" + "slices" "strings" "sync" "time" @@ -48,11 +49,11 @@ func (c *CustomName) toValues(exclude ...string) map[string]string { c.DbURL: fmt.Sprintf("postgresql://%s@%s:%d/postgres", url.UserPassword("postgres", utils.Config.Db.Password), utils.Config.Hostname, utils.Config.Db.Port), } - apiEnabled := utils.Config.Api.Enabled && !utils.SliceContains(exclude, utils.RestId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Api.Image)) - studioEnabled := utils.Config.Studio.Enabled && !utils.SliceContains(exclude, utils.StudioId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Studio.Image)) - authEnabled := utils.Config.Auth.Enabled && !utils.SliceContains(exclude, utils.GotrueId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Auth.Image)) - inbucketEnabled := utils.Config.Inbucket.Enabled && !utils.SliceContains(exclude, utils.InbucketId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Inbucket.Image)) - storageEnabled := utils.Config.Storage.Enabled && !utils.SliceContains(exclude, utils.StorageId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Storage.Image)) + apiEnabled := utils.Config.Api.Enabled && !slices.Contains(exclude, utils.RestId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Api.Image)) + studioEnabled := utils.Config.Studio.Enabled && !slices.Contains(exclude, utils.StudioId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Studio.Image)) + authEnabled := utils.Config.Auth.Enabled && !slices.Contains(exclude, utils.GotrueId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Auth.Image)) + inbucketEnabled := utils.Config.Inbucket.Enabled && !slices.Contains(exclude, utils.InbucketId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Inbucket.Image)) + storageEnabled := utils.Config.Storage.Enabled && !slices.Contains(exclude, utils.StorageId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Storage.Image)) if apiEnabled { values[c.ApiURL] = utils.Config.Api.ExternalUrl diff --git a/internal/storage/ls/ls_test.go b/internal/storage/ls/ls_test.go index 27c388f5f..e32c5b845 100644 --- a/internal/storage/ls/ls_test.go +++ b/internal/storage/ls/ls_test.go @@ -185,7 +185,7 @@ func TestListStoragePaths(t *testing.T) { defer gock.OffAll() expected := make([]string, storage.PAGE_LIMIT) resp := make([]storage.ObjectResponse, storage.PAGE_LIMIT) - for i := 0; i < len(resp); i++ { + for i := range resp { resp[i] = storage.ObjectResponse{Name: fmt.Sprintf("dir_%d", i)} expected[i] = resp[i].Name + "/" } diff --git a/internal/utils/connect.go b/internal/utils/connect.go index 1527a020d..3223bb032 100644 --- a/internal/utils/connect.go +++ b/internal/utils/connect.go @@ -59,7 +59,7 @@ func GetPoolerConfig(projectRef string) *pgconn.Config { } // Verify that the pooler username matches the database host being connected to if _, ref, found := strings.Cut(poolerConfig.User, "."); !found { - for _, option := range strings.Split(poolerConfig.RuntimeParams["options"], ",") { + for option := range strings.SplitSeq(poolerConfig.RuntimeParams["options"], ",") { key, value, found := strings.Cut(option, "=") if found && key == "reference" && value != projectRef { fmt.Fprintln(logger, "Pooler options does not match project ref:", projectRef) diff --git a/internal/utils/container_output.go b/internal/utils/container_output.go index ccbbab033..bd3e19d1f 100644 --- a/internal/utils/container_output.go +++ b/internal/utils/container_output.go @@ -8,6 +8,7 @@ import ( "io" "os" "regexp" + "slices" "strconv" "strings" @@ -178,12 +179,7 @@ func ProcessDiffOutput(diffBytes []byte) ([]byte, error) { } isSchemaIgnored := func(schema string) bool { - for _, s := range InternalSchemas { - if s == schema { - return true - } - } - return false + return slices.Contains(InternalSchemas, schema) } if isSchemaIgnored(diffEntry.GroupName) || diff --git a/internal/utils/docker.go b/internal/utils/docker.go index 7e908649e..6da0d5aa6 100644 --- a/internal/utils/docker.go +++ b/internal/utils/docker.go @@ -224,7 +224,7 @@ var timeUnit = time.Second func DockerImagePullWithRetry(ctx context.Context, image string, retries int) error { err := DockerImagePull(ctx, image, os.Stderr) - for i := 0; i < retries; i++ { + for i := range retries { if err == nil || errors.Is(ctx.Err(), context.Canceled) { break } diff --git a/internal/utils/enum.go b/internal/utils/enum.go index 6d800d6a0..06cac719d 100644 --- a/internal/utils/enum.go +++ b/internal/utils/enum.go @@ -2,6 +2,7 @@ package utils import ( "fmt" + "slices" "strings" "github.com/go-errors/errors" @@ -18,7 +19,7 @@ func (a EnumFlag) String() string { } func (a *EnumFlag) Set(p string) error { - if !SliceContains(a.Allowed, p) { + if !slices.Contains(a.Allowed, p) { return errors.Errorf("must be one of [ %s ]", strings.Join(a.Allowed, " | ")) } a.Value = p diff --git a/internal/utils/flags/db_url.go b/internal/utils/flags/db_url.go index 152900ba2..7ee2417e5 100644 --- a/internal/utils/flags/db_url.go +++ b/internal/utils/flags/db_url.go @@ -124,6 +124,8 @@ func NewDbConfigWithPassword(ctx context.Context, projectRef string) pgconn.Conf loginRole, err := initLoginRole(ctx, projectRef, config) if err == nil { return loginRole + } else if errors.Is(err, context.Canceled) { + return config } // Proceed with password prompt fmt.Fprintln(utils.GetDebugLogger(), err) @@ -202,7 +204,7 @@ func PromptPassword(stdin *os.File) string { charset := string(config.LowerUpperLettersDigits.ToChar()) charset = strings.ReplaceAll(charset, ":", "") maxRange := big.NewInt(int64(len(charset))) - for i := 0; i < PASSWORD_LENGTH; i++ { + for range PASSWORD_LENGTH { random, err := rand.Int(rand.Reader, maxRange) if err != nil { fmt.Fprintln(os.Stderr, "Failed to randomise password:", err) diff --git a/internal/utils/misc.go b/internal/utils/misc.go index 84d65eaeb..f81b792f5 100644 --- a/internal/utils/misc.go +++ b/internal/utils/misc.go @@ -69,6 +69,7 @@ var ( GotrueVersionPath = filepath.Join(TempDir, "gotrue-version") RestVersionPath = filepath.Join(TempDir, "rest-version") StorageVersionPath = filepath.Join(TempDir, "storage-version") + StorageMigrationPath = filepath.Join(TempDir, "storage-migration") StudioVersionPath = filepath.Join(TempDir, "studio-version") PgmetaVersionPath = filepath.Join(TempDir, "pgmeta-version") PoolerVersionPath = filepath.Join(TempDir, "pooler-version") diff --git a/internal/utils/output_test.go b/internal/utils/output_test.go index daaa947c1..a04be2536 100644 --- a/internal/utils/output_test.go +++ b/internal/utils/output_test.go @@ -34,9 +34,9 @@ func TestEncodeOutput(t *testing.T) { }) t.Run("encodes json format", func(t *testing.T) { - input := map[string]interface{}{ + input := map[string]any{ "name": "test", - "config": map[string]interface{}{ + "config": map[string]any{ "port": 5432, "enabled": true, }, @@ -56,9 +56,9 @@ func TestEncodeOutput(t *testing.T) { }) t.Run("encodes yaml format", func(t *testing.T) { - input := map[string]interface{}{ + input := map[string]any{ "name": "test", - "config": map[string]interface{}{ + "config": map[string]any{ "port": 5432, "enabled": true, }, @@ -75,9 +75,9 @@ name: test }) t.Run("encodes toml format", func(t *testing.T) { - input := map[string]interface{}{ + input := map[string]any{ "name": "test", - "config": map[string]interface{}{ + "config": map[string]any{ "port": 5432, "enabled": true, }, @@ -101,9 +101,9 @@ name: test }) t.Run("handles complex nested structures", func(t *testing.T) { - input := map[string]interface{}{ - "database": map[string]interface{}{ - "connections": []map[string]interface{}{ + input := map[string]any{ + "database": map[string]any{ + "connections": []map[string]any{ { "host": "localhost", "port": 5432, @@ -113,7 +113,7 @@ name: test "port": 6543, }, }, - "settings": map[string]interface{}{ + "settings": map[string]any{ "max_connections": 100, "ssl_enabled": true, }, diff --git a/internal/utils/prompt.go b/internal/utils/prompt.go index 5589f49d6..3e121f092 100644 --- a/internal/utils/prompt.go +++ b/internal/utils/prompt.go @@ -113,10 +113,7 @@ func PromptChoice(ctx context.Context, title string, items []PromptItem, opts .. listItems = append(listItems, v) } // Create list model - height := len(listItems) * 4 - if height > 14 { - height = 14 - } + height := min(len(listItems)*4, 14) l := list.New(listItems, itemDelegate{}, 0, height) l.Title = title l.SetShowStatusBar(false) diff --git a/internal/utils/slice.go b/internal/utils/slice.go index 3d036c19d..9babf6c59 100644 --- a/internal/utils/slice.go +++ b/internal/utils/slice.go @@ -1,26 +1,5 @@ package utils -func SliceContains[T comparable](s []T, e T) bool { - for _, a := range s { - if a == e { - return true - } - } - return false -} - -func SliceEqual[T comparable](a, b []T) bool { - if len(a) != len(b) { - return false - } - for i, v := range a { - if v != b[i] { - return false - } - } - return true -} - func RemoveDuplicates[T comparable](slice []T) (result []T) { set := make(map[T]struct{}) for _, item := range slice { diff --git a/internal/utils/slice_test.go b/internal/utils/slice_test.go index 47bdea171..3c2a3e957 100644 --- a/internal/utils/slice_test.go +++ b/internal/utils/slice_test.go @@ -6,41 +6,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSliceEqual(t *testing.T) { - t.Run("different slices", func(t *testing.T) { - assert.False(t, SliceEqual([]string{"a"}, []string{"b"})) - }) - - t.Run("different lengths", func(t *testing.T) { - assert.False(t, SliceEqual([]string{"a"}, []string{"a", "b"})) - assert.False(t, SliceEqual([]string{"a", "b"}, []string{"a"})) - }) - - t.Run("equal slices", func(t *testing.T) { - assert.True(t, SliceEqual([]string{"a", "b"}, []string{"a", "b"})) - assert.True(t, SliceEqual([]int{1, 2, 3}, []int{1, 2, 3})) - }) - - t.Run("empty slices", func(t *testing.T) { - assert.True(t, SliceEqual([]string{}, []string{})) - }) -} - -func TestSliceContains(t *testing.T) { - t.Run("not contains element", func(t *testing.T) { - assert.False(t, SliceContains([]string{"a"}, "b")) - }) - - t.Run("contains element", func(t *testing.T) { - assert.True(t, SliceContains([]string{"a", "b", "c"}, "b")) - assert.True(t, SliceContains([]int{1, 2, 3}, 2)) - }) - - t.Run("empty slice", func(t *testing.T) { - assert.False(t, SliceContains([]string{}, "a")) - }) -} - func TestRemoveDuplicates(t *testing.T) { t.Run("string slice", func(t *testing.T) { input := []string{"a", "b", "a", "c", "b", "d"} diff --git a/pkg/api/client.gen.go b/pkg/api/client.gen.go index ac1f7d1c1..e8d6bcf49 100644 --- a/pkg/api/client.gen.go +++ b/pkg/api/client.gen.go @@ -3934,6 +3934,22 @@ func NewV1GetAvailableRegionsRequest(server string, params *V1GetAvailableRegion } + if params.DesiredInstanceSize != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "desired_instance_size", runtime.ParamLocationQuery, *params.DesiredInstanceSize); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + queryURL.RawQuery = queryValues.Encode() } diff --git a/pkg/api/types.gen.go b/pkg/api/types.gen.go index 2b7ee8870..192bf38d9 100644 --- a/pkg/api/types.gen.go +++ b/pkg/api/types.gen.go @@ -1369,6 +1369,30 @@ const ( SA V1GetAvailableRegionsParamsContinent = "SA" ) +// Defines values for V1GetAvailableRegionsParamsDesiredInstanceSize. +const ( + Large V1GetAvailableRegionsParamsDesiredInstanceSize = "large" + Medium V1GetAvailableRegionsParamsDesiredInstanceSize = "medium" + Micro V1GetAvailableRegionsParamsDesiredInstanceSize = "micro" + N12xlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "12xlarge" + N16xlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "16xlarge" + N24xlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "24xlarge" + N24xlargeHighMemory V1GetAvailableRegionsParamsDesiredInstanceSize = "24xlarge_high_memory" + N24xlargeOptimizedCpu V1GetAvailableRegionsParamsDesiredInstanceSize = "24xlarge_optimized_cpu" + N24xlargeOptimizedMemory V1GetAvailableRegionsParamsDesiredInstanceSize = "24xlarge_optimized_memory" + N2xlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "2xlarge" + N48xlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "48xlarge" + N48xlargeHighMemory V1GetAvailableRegionsParamsDesiredInstanceSize = "48xlarge_high_memory" + N48xlargeOptimizedCpu V1GetAvailableRegionsParamsDesiredInstanceSize = "48xlarge_optimized_cpu" + N48xlargeOptimizedMemory V1GetAvailableRegionsParamsDesiredInstanceSize = "48xlarge_optimized_memory" + N4xlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "4xlarge" + N8xlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "8xlarge" + Nano V1GetAvailableRegionsParamsDesiredInstanceSize = "nano" + Pico V1GetAvailableRegionsParamsDesiredInstanceSize = "pico" + Small V1GetAvailableRegionsParamsDesiredInstanceSize = "small" + Xlarge V1GetAvailableRegionsParamsDesiredInstanceSize = "xlarge" +) + // Defines values for V1GetSecurityAdvisorsParamsLintType. const ( Sql V1GetSecurityAdvisorsParamsLintType = "sql" @@ -1837,6 +1861,7 @@ type BranchResponse struct { // Deprecated: LatestCheckRunId *float32 `json:"latest_check_run_id,omitempty"` Name string `json:"name"` + NotifyUrl *string `json:"notify_url,omitempty"` ParentProjectRef string `json:"parent_project_ref"` Persistent bool `json:"persistent"` PrNumber *int32 `json:"pr_number,omitempty"` @@ -1915,7 +1940,10 @@ type CreateBranchBody struct { DesiredInstanceSize *CreateBranchBodyDesiredInstanceSize `json:"desired_instance_size,omitempty"` GitBranch *string `json:"git_branch,omitempty"` IsDefault *bool `json:"is_default,omitempty"` - Persistent *bool `json:"persistent,omitempty"` + + // NotifyUrl HTTP endpoint to receive branch status updates. + NotifyUrl *string `json:"notify_url,omitempty"` + Persistent *bool `json:"persistent,omitempty"` // PostgresEngine Postgres engine version. If not provided, the latest version will be used. PostgresEngine *CreateBranchBodyPostgresEngine `json:"postgres_engine,omitempty"` @@ -2756,6 +2784,7 @@ type PgsodiumConfigResponse struct { // PostgresConfigResponse defines model for PostgresConfigResponse. type PostgresConfigResponse struct { EffectiveCacheSize *string `json:"effective_cache_size,omitempty"` + HotStandbyFeedback *bool `json:"hot_standby_feedback,omitempty"` LogicalDecodingWorkMem *string `json:"logical_decoding_work_mem,omitempty"` MaintenanceWorkMem *string `json:"maintenance_work_mem,omitempty"` MaxConnections *int `json:"max_connections,omitempty"` @@ -3369,8 +3398,11 @@ type UpdateAuthConfigBodySmsProvider string // UpdateBranchBody defines model for UpdateBranchBody. type UpdateBranchBody struct { - BranchName *string `json:"branch_name,omitempty"` - GitBranch *string `json:"git_branch,omitempty"` + BranchName *string `json:"branch_name,omitempty"` + GitBranch *string `json:"git_branch,omitempty"` + + // NotifyUrl HTTP endpoint to receive branch status updates. + NotifyUrl *string `json:"notify_url,omitempty"` Persistent *bool `json:"persistent,omitempty"` RequestReview *bool `json:"request_review,omitempty"` @@ -3449,6 +3481,7 @@ type UpdatePgsodiumConfigBody struct { // UpdatePostgresConfigBody defines model for UpdatePostgresConfigBody. type UpdatePostgresConfigBody struct { EffectiveCacheSize *string `json:"effective_cache_size,omitempty"` + HotStandbyFeedback *bool `json:"hot_standby_feedback,omitempty"` LogicalDecodingWorkMem *string `json:"logical_decoding_work_mem,omitempty"` MaintenanceWorkMem *string `json:"maintenance_work_mem,omitempty"` MaxConnections *int `json:"max_connections,omitempty"` @@ -4014,6 +4047,11 @@ type V1ServiceHealthResponseInfo1 struct { Healthy bool `json:"healthy"` } +// V1ServiceHealthResponseInfo2 defines model for . +type V1ServiceHealthResponseInfo2 struct { + DbSchema string `json:"db_schema"` +} + // V1ServiceHealthResponse_Info defines model for V1ServiceHealthResponse.Info. type V1ServiceHealthResponse_Info struct { union json.RawMessage @@ -4128,13 +4166,19 @@ type V1GetAvailableRegionsParams struct { // OrganizationSlug Slug of your organization OrganizationSlug string `form:"organization_slug" json:"organization_slug"` - // Continent Continent code to determine regional recommendations + // Continent Continent code to determine regional recommendations: NA (North America), SA (South America), EU (Europe), AF (Africa), AS (Asia), OC (Oceania), AN (Antarctica) Continent *V1GetAvailableRegionsParamsContinent `form:"continent,omitempty" json:"continent,omitempty"` + + // DesiredInstanceSize Desired instance size + DesiredInstanceSize *V1GetAvailableRegionsParamsDesiredInstanceSize `form:"desired_instance_size,omitempty" json:"desired_instance_size,omitempty"` } // V1GetAvailableRegionsParamsContinent defines parameters for V1GetAvailableRegions. type V1GetAvailableRegionsParamsContinent string +// V1GetAvailableRegionsParamsDesiredInstanceSize defines parameters for V1GetAvailableRegions. +type V1GetAvailableRegionsParamsDesiredInstanceSize string + // V1ListActionRunsParams defines parameters for V1ListActionRuns. type V1ListActionRunsParams struct { Offset *float32 `form:"offset,omitempty" json:"offset,omitempty"` @@ -5522,6 +5566,32 @@ func (t *V1ServiceHealthResponse_Info) MergeV1ServiceHealthResponseInfo1(v V1Ser return err } +// AsV1ServiceHealthResponseInfo2 returns the union data inside the V1ServiceHealthResponse_Info as a V1ServiceHealthResponseInfo2 +func (t V1ServiceHealthResponse_Info) AsV1ServiceHealthResponseInfo2() (V1ServiceHealthResponseInfo2, error) { + var body V1ServiceHealthResponseInfo2 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromV1ServiceHealthResponseInfo2 overwrites any union data inside the V1ServiceHealthResponse_Info as the provided V1ServiceHealthResponseInfo2 +func (t *V1ServiceHealthResponse_Info) FromV1ServiceHealthResponseInfo2(v V1ServiceHealthResponseInfo2) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeV1ServiceHealthResponseInfo2 performs a merge with any union data inside the V1ServiceHealthResponse_Info, using the provided V1ServiceHealthResponseInfo2 +func (t *V1ServiceHealthResponse_Info) MergeV1ServiceHealthResponseInfo2(v V1ServiceHealthResponseInfo2) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + func (t V1ServiceHealthResponse_Info) MarshalJSON() ([]byte, error) { b, err := t.union.MarshalJSON() return b, err diff --git a/pkg/config/auth.go b/pkg/config/auth.go index c8f017a5c..82fc0eef7 100644 --- a/pkg/config/auth.go +++ b/pkg/config/auth.go @@ -1,6 +1,7 @@ package config import ( + "slices" "strconv" "strings" "time" @@ -24,7 +25,7 @@ const ( func (r *PasswordRequirements) UnmarshalText(text []byte) error { allowed := []PasswordRequirements{NoRequirements, LettersDigits, LowerUpperLettersDigits, LowerUpperLettersDigitsSymbols} - if *r = PasswordRequirements(text); !sliceContains(allowed, *r) { + if *r = PasswordRequirements(text); !slices.Contains(allowed, *r) { return errors.Errorf("must be one of %v", allowed) } return nil @@ -63,7 +64,7 @@ const ( func (p *CaptchaProvider) UnmarshalText(text []byte) error { allowed := []CaptchaProvider{HCaptchaProvider, TurnstileProvider} - if *p = CaptchaProvider(text); !sliceContains(allowed, *p) { + if *p = CaptchaProvider(text); !slices.Contains(allowed, *p) { return errors.Errorf("must be one of %v", allowed) } return nil @@ -78,7 +79,7 @@ const ( func (p *Algorithm) UnmarshalText(text []byte) error { allowed := []Algorithm{AlgRS256, AlgES256} - if *p = Algorithm(text); !sliceContains(allowed, *p) { + if *p = Algorithm(text); !slices.Contains(allowed, *p) { return errors.Errorf("must be one of %v", allowed) } return nil diff --git a/pkg/config/config.go b/pkg/config/config.go index ea49a76ca..f909d4e88 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -17,6 +17,7 @@ import ( "path" "path/filepath" "regexp" + "slices" "sort" "strconv" "strings" @@ -58,7 +59,7 @@ const ( func (b *LogflareBackend) UnmarshalText(text []byte) error { allowed := []LogflareBackend{LogflarePostgres, LogflareBigQuery} - if *b = LogflareBackend(text); !sliceContains(allowed, *b) { + if *b = LogflareBackend(text); !slices.Contains(allowed, *b) { return errors.Errorf("must be one of %v", allowed) } return nil @@ -73,7 +74,7 @@ const ( func (f *AddressFamily) UnmarshalText(text []byte) error { allowed := []AddressFamily{AddressIPv6, AddressIPv4} - if *f = AddressFamily(text); !sliceContains(allowed, *f) { + if *f = AddressFamily(text); !slices.Contains(allowed, *f) { return errors.Errorf("must be one of %v", allowed) } return nil @@ -88,7 +89,7 @@ const ( func (p *RequestPolicy) UnmarshalText(text []byte) error { allowed := []RequestPolicy{PolicyPerWorker, PolicyOneshot} - if *p = RequestPolicy(text); !sliceContains(allowed, *p) { + if *p = RequestPolicy(text); !slices.Contains(allowed, *p) { return errors.Errorf("must be one of %v", allowed) } return nil @@ -606,11 +607,14 @@ func (c *config) Load(path string, fsys fs.FS) error { } } 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 + // Only replace image if local storage version is newer + if i := strings.IndexByte(Images.Storage, ':'); VersionCompare(string(version), Images.Storage[i+1:]) > 0 { + c.Storage.Image = replaceImageTag(Images.Storage, string(version)) } } + if version, err := fs.ReadFile(fsys, builder.StorageMigrationPath); err == nil && len(version) > 0 { + c.Storage.TargetMigration = string(version) + } if version, err := fs.ReadFile(fsys, builder.EdgeRuntimeVersionPath); err == nil && len(version) > 0 { c.EdgeRuntime.Image = replaceImageTag(Images.EdgeRuntime, string(version)) } @@ -1133,7 +1137,7 @@ func (e external) validate() (err error) { if provider.ClientId == "" { return errors.Errorf("Missing required field in config: auth.external.%s.client_id", ext) } - if !sliceContains([]string{"apple", "google"}, ext) && len(provider.Secret.Value) == 0 { + if !slices.Contains([]string{"apple", "google"}, ext) && len(provider.Secret.Value) == 0 { return errors.Errorf("Missing required field in config: auth.external.%s.secret", ext) } if err := assertEnvLoaded(provider.ClientId); err != nil { diff --git a/pkg/config/constants.go b/pkg/config/constants.go index 466fbe3eb..08d572d2d 100644 --- a/pkg/config/constants.go +++ b/pkg/config/constants.go @@ -12,7 +12,7 @@ const ( pg13 = "supabase/postgres:13.3.0" pg14 = "supabase/postgres:14.1.0.89" pg15 = "supabase/postgres:15.8.1.085" - deno1 = "supabase/edge-runtime:v1.68.3" + deno1 = "supabase/edge-runtime:v1.68.4" ) type images struct { diff --git a/pkg/config/db.go b/pkg/config/db.go index 740dabf53..b2b947667 100644 --- a/pkg/config/db.go +++ b/pkg/config/db.go @@ -2,6 +2,7 @@ package config import ( "bytes" + "slices" "github.com/go-errors/errors" v1API "github.com/supabase/cli/pkg/api" @@ -18,7 +19,7 @@ const ( func (m *PoolMode) UnmarshalText(text []byte) error { allowed := []PoolMode{TransactionMode, SessionMode} - if *m = PoolMode(text); !sliceContains(allowed, *m) { + if *m = PoolMode(text); !slices.Contains(allowed, *m) { return errors.Errorf("must be one of %v", allowed) } return nil @@ -34,7 +35,7 @@ const ( func (r *SessionReplicationRole) UnmarshalText(text []byte) error { allowed := []SessionReplicationRole{SessionReplicationRoleOrigin, SessionReplicationRoleReplica, SessionReplicationRoleLocal} - if *r = SessionReplicationRole(text); !sliceContains(allowed, *r) { + if *r = SessionReplicationRole(text); !slices.Contains(allowed, *r) { return errors.Errorf("must be one of %v", allowed) } return nil diff --git a/pkg/config/templates/Dockerfile b/pkg/config/templates/Dockerfile index f26370c15..526aa9158 100644 --- a/pkg/config/templates/Dockerfile +++ b/pkg/config/templates/Dockerfile @@ -1,17 +1,17 @@ # Exposed for updates by .github/dependabot.yml -FROM supabase/postgres:17.6.1.017 AS pg +FROM supabase/postgres:17.6.1.024 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:v13.0.7 AS postgrest -FROM supabase/postgres-meta:v0.91.7 AS pgmeta -FROM supabase/studio:2025.10.09-sha-433e578 AS studio +FROM supabase/postgres-meta:v0.93.0 AS pgmeta +FROM supabase/studio:2025.10.20-sha-5005fc6 AS studio FROM darthsim/imgproxy:v3.8.0 AS imgproxy -FROM supabase/edge-runtime:v1.69.12 AS edgeruntime +FROM supabase/edge-runtime:v1.69.14 AS edgeruntime FROM timberio/vector:0.28.1-alpine AS vector FROM supabase/supavisor:2.7.3 AS supavisor FROM supabase/gotrue:v2.180.0 AS gotrue -FROM supabase/realtime:v2.53.2 AS realtime +FROM supabase/realtime:v2.54.4 AS realtime FROM supabase/storage-api:v1.28.1 AS storage FROM supabase/logflare:1.23.0 AS logflare # Append to JobImages when adding new dependencies below diff --git a/pkg/config/utils.go b/pkg/config/utils.go index e4a77aef4..829ec67a1 100644 --- a/pkg/config/utils.go +++ b/pkg/config/utils.go @@ -21,6 +21,7 @@ type pathBuilder struct { GotrueVersionPath string RestVersionPath string StorageVersionPath string + StorageMigrationPath string StudioVersionPath string PgmetaVersionPath string PoolerVersionPath string @@ -55,6 +56,7 @@ func NewPathBuilder(configPath string) pathBuilder { GotrueVersionPath: filepath.Join(base, ".temp", "gotrue-version"), RestVersionPath: filepath.Join(base, ".temp", "rest-version"), StorageVersionPath: filepath.Join(base, ".temp", "storage-version"), + StorageMigrationPath: filepath.Join(base, ".temp", "storage-migration"), EdgeRuntimeVersionPath: filepath.Join(base, ".temp", "edge-runtime-version"), StudioVersionPath: filepath.Join(base, ".temp", "studio-version"), PgmetaVersionPath: filepath.Join(base, ".temp", "pgmeta-version"), @@ -72,15 +74,6 @@ func NewPathBuilder(configPath string) pathBuilder { } } -func sliceContains[T comparable](s []T, e T) bool { - for _, a := range s { - if a == e { - return true - } - } - return false -} - func replaceImageTag(image string, tag string) string { index := strings.IndexByte(image, ':') return image[:index+1] + strings.TrimSpace(tag) diff --git a/pkg/migration/apply.go b/pkg/migration/apply.go index ac99f7b23..e40b58be2 100644 --- a/pkg/migration/apply.go +++ b/pkg/migration/apply.go @@ -62,6 +62,11 @@ func ApplyMigrations(ctx context.Context, pending []string, conn *pgx.Conn, fsys for _, path := range pending { filename := filepath.Base(path) fmt.Fprintf(os.Stderr, "Applying migration %s...\n", filename) + // Reset all connection settings that might have been modified by another statement on the same connection + // eg: `SELECT pg_catalog.set_config('search_path', '', false);` + if _, err := conn.Exec(ctx, "RESET ALL"); err != nil { + return errors.Errorf("failed to reset connection state: %v", err) + } if migration, err := NewMigrationFromFile(path, fsys); err != nil { return err } else if err := migration.ExecBatch(ctx, conn); err != nil { diff --git a/pkg/migration/apply_test.go b/pkg/migration/apply_test.go index 65a75028a..e6df97721 100644 --- a/pkg/migration/apply_test.go +++ b/pkg/migration/apply_test.go @@ -108,6 +108,8 @@ func TestApplyMigrations(t *testing.T) { conn := pgtest.NewConn() defer conn.Close(t) mockMigrationHistory(conn). + Query("RESET ALL"). + Reply("RESET"). Query(testSchema). Reply("CREATE SCHEMA"). Query(INSERT_MIGRATION_VERSION, "0", "schema", []string{testSchema}). @@ -143,7 +145,9 @@ func TestApplyMigrations(t *testing.T) { // Setup mock postgres conn := pgtest.NewConn() defer conn.Close(t) - mockMigrationHistory(conn) + mockMigrationHistory(conn). + Query("RESET ALL"). + Reply("RESET") // Run test err := ApplyMigrations(context.Background(), pending, conn.MockClient(t), fsys) // Check error @@ -155,6 +159,8 @@ func TestApplyMigrations(t *testing.T) { conn := pgtest.NewConn() defer conn.Close(t) mockMigrationHistory(conn). + Query("RESET ALL"). + Reply("RESET"). Query(testSchema). ReplyError(pgerrcode.UndefinedTable, `relation "supabase_migrations.schema_migrations" does not exist`). Query(INSERT_MIGRATION_VERSION, "0", "schema", []string{testSchema}). @@ -164,6 +170,20 @@ func TestApplyMigrations(t *testing.T) { // Check error assert.ErrorContains(t, err, `ERROR: relation "supabase_migrations.schema_migrations" does not exist (SQLSTATE 42P01)`) }) + + t.Run("throws error when RESET ALL fails", func(t *testing.T) { + // Setup mock postgres + conn := pgtest.NewConn() + defer conn.Close(t) + mockMigrationHistory(conn). + Query("RESET ALL"). + ReplyError(pgerrcode.InsufficientPrivilege, "permission denied for RESET ALL") + // Run test + err := ApplyMigrations(context.Background(), pending, conn.MockClient(t), testMigrations) + // Check error + assert.ErrorContains(t, err, "failed to reset connection state") + assert.ErrorContains(t, err, "ERROR: permission denied for RESET ALL (SQLSTATE 42501)") + }) } func mockMigrationHistory(conn *pgtest.MockConn) *pgtest.MockConn { diff --git a/tools/selfhost/main.go b/tools/selfhost/main.go index be7886ae9..f4eee46da 100644 --- a/tools/selfhost/main.go +++ b/tools/selfhost/main.go @@ -61,7 +61,8 @@ func updateSelfHosted(ctx context.Context, branch string) error { } func getStableVersions() map[string]string { - images := append(config.Images.Services(), config.Images.Pg) + // TODO(qiao): include config.Images.Pg after self-hosted has upgraded to 17 + images := config.Images.Services() result := make(map[string]string, len(images)) for _, img := range images { parts := strings.Split(img, ":")