Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 516
fix(cli): provision platform baseline in declarative baseline catalog#5521
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
210d0e59f63a3c3dfa1cad264376File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -31,10 +31,16 @@ const ( | ||
| // pgDeltaTempDir namespaces pg-delta artifacts under .temp to make ownership | ||
| // and cleanup intent explicit. | ||
| pgDeltaTempDir = "pgdelta" | ||
| // baselineCatalogName caches the catalog of an empty shadow database. | ||
| // baselineCatalogName caches the catalog of a shadow database with the Supabase | ||
| // platform baseline (auth/storage/realtime) provisioned but no user migrations | ||
| // applied — equivalent to diff.MigrateShadowDatabase with zero migrations. | ||
| // | ||
| // It is used as the "source" baseline when generating declarative files from | ||
| // a real database target. | ||
| // It is used as the "source" baseline both when generating declarative files | ||
| // from a real database target and when syncing with no local migrations, so it | ||
| // must stay in parity with the declarative target's platform baseline. The "%s" | ||
| // is a key (see baselineCatalogKey) derived from the image plus every setup | ||
| // input that shapes the baseline, so config/roles changes self-invalidate the | ||
| // cache rather than reusing a stale snapshot. | ||
| baselineCatalogName = "catalog-baseline-%s.json" | ||
| // declarativeCatalogName stores catalogs keyed by declarative-content hash. | ||
| declarativeCatalogName = "catalog-%s-declarative-%s-%d.json" | ||
| @@ -58,11 +64,19 @@ var ( | ||
| exportCatalog = diff.ExportCatalogPgDelta | ||
| applyDeclarative = pgdelta.ApplyDeclarative | ||
| declarativeExportRef = diff.DeclarativeExportPgDeltaRef | ||
| // diffPgDeltaRef diffs a source catalog against a target catalog. It is a | ||
| // package var so tests can exercise the full generate -> sync flow without the | ||
| // real pg-delta runtime. | ||
| diffPgDeltaRef = diff.DiffPgDeltaRef | ||
| // setupShadowDatabase provisions the Supabase platform baseline (auth/storage/ | ||
| // realtime) on a shadow database before declarative schemas are applied, so | ||
| // Supabase-managed dependencies (auth.sessions, auth.jwt(), ...) resolve. It is | ||
| // a package var so tests can inject a no-op without a real shadow database. | ||
| setupShadowDatabase = diff.SetupShadowDatabase | ||
| // createShadow provisions a healthy shadow database container. It is a package | ||
| // var so tests can exercise the baseline/migrations/declarative paths without a | ||
| // real Docker daemon. | ||
| createShadow = createShadowContainer | ||
| // generateBaselineCatalogRefResolver allows Generate to reuse a freshly | ||
| // provisioned baseline shadow for declarative cache warmup. | ||
| generateBaselineCatalogRefResolver = getGenerateBaselineCatalogRef | ||
| @@ -123,14 +137,10 @@ func Generate(ctx context.Context, schema []string, config pgconn.Config, overwr | ||
| // can reuse it without provisioning another shadow database. | ||
| if !noCache { | ||
| if baseline.shadow != nil { | ||
| // The baseline catalog was already exported from the empty image | ||
| // baseline above. Set up the platform baseline on the reused shadow | ||
| // before applying declarative schemas so Supabase-managed dependencies | ||
| // (auth.sessions, auth.jwt(), ...) resolve during cache warmup. | ||
| if err := setupShadowDatabase(ctx, baseline.shadow.container, fsys, options...); err != nil { | ||
| return err | ||
| } | ||
| hash, err := hashDeclarativeSchemas(fsys) | ||
| // The reused baseline shadow already has the platform baseline | ||
| // provisioned (getGenerateBaselineCatalogRef), so apply declarative | ||
| // schemas directly on top of it without setting it up again. | ||
| hash, err := declarativeCatalogCacheKey(fsys) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| @@ -172,7 +182,7 @@ func DiffDeclarativeToMigrations(ctx context.Context, schema []string, noCache b | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| out, err := diff.DiffPgDeltaRef(ctx, sourceRef, targetRef, schema, pgDeltaFormatOptions(), options...) | ||
| out, err := diffPgDeltaRef(ctx, sourceRef, targetRef, schema, pgDeltaFormatOptions(), options...) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| @@ -294,7 +304,10 @@ func updateDeclarativeSchemaPathsConfig(fsys afero.Fs) error { | ||
| } | ||
| func getGenerateBaselineCatalogRef(ctx context.Context, noCache bool, fsys afero.Fs, options ...func(*pgx.ConnConfig)) (generateBaselineCatalogRef, error) { | ||
| cachePath := filepath.Join(pgDeltaTempPath(), fmt.Sprintf(baselineCatalogName, baselineVersionToken())) | ||
| cachePath, err := baselineCatalogPath(fsys) | ||
| if err != nil { | ||
| return generateBaselineCatalogRef{}, err | ||
| } | ||
| if !noCache { | ||
| if ok, err := afero.Exists(fsys, cachePath); err == nil && ok { | ||
| return generateBaselineCatalogRef{ref: cachePath}, nil | ||
| @@ -308,6 +321,18 @@ func getGenerateBaselineCatalogRef(ctx context.Context, noCache bool, fsys afero | ||
| container: shadowID, | ||
| config: config, | ||
| } | ||
| // Provision the Supabase platform baseline before exporting so the baseline | ||
| // catalog represents "platform baseline, no user migrations" — the same | ||
| // semantics as diff.MigrateShadowDatabase with zero migrations. This baseline is | ||
| // reused as the diff source by both Generate (against the live database) and | ||
| // sync-with-no-migrations (getMigrationsCatalogRef). Its starting point must | ||
| // match the declarative target, which also sets up the platform baseline; | ||
| // otherwise platform objects (auth/storage/realtime) surface as spurious | ||
| // additions in generated migrations. | ||
| if err := setupShadowDatabase(ctx, shadow.container, fsys, options...); err != nil { | ||
avallete marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. avallete marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| shadow.cleanup() | ||
| return generateBaselineCatalogRef{}, err | ||
| } | ||
| snapshot, err := exportCatalog(ctx, utils.ToPostgresURL(config), "postgres", options...) | ||
| if err != nil { | ||
| shadow.cleanup() | ||
| @@ -345,21 +370,31 @@ func getMigrationsCatalogRef(ctx context.Context, noCache bool, fsys afero.Fs, p | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| // For sync with no local migrations, reuse an existing baseline | ||
| // snapshot instead of provisioning a fresh shadow database. | ||
| if !noCache && len(migrations) == 0 { | ||
| baselinePath := filepath.Join(pgDeltaTempPath(), fmt.Sprintf(baselineCatalogName, baselineVersionToken())) | ||
| if ok, err := afero.Exists(fsys, baselinePath); err != nil { | ||
| // With no local migrations, the migrations catalog is exactly the platform | ||
| // baseline, so it is cached under the setup-keyed baseline path rather than the | ||
| // migrations-hash cache. The migrations-hash cache is not setup-aware, so an | ||
| // older empty-migrations snapshot from a different platform setup must not be | ||
| // reused as the no-migration sync source. | ||
| zeroMigrations := len(migrations) == 0 | ||
| var baselinePath string | ||
| if zeroMigrations { | ||
| baselinePath, err = baselineCatalogPath(fsys) | ||
| if err != nil { | ||
| return "", err | ||
| } else if ok { | ||
| return baselinePath, nil | ||
| } | ||
| if !noCache { | ||
| if ok, err := afero.Exists(fsys, baselinePath); err != nil { | ||
| return "", err | ||
| } else if ok { | ||
| return baselinePath, nil | ||
| } | ||
| } | ||
| } | ||
| hash, err := pgcache.HashMigrations(fsys) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| if !noCache { | ||
| if !noCache && !zeroMigrations { | ||
| if cachePath, ok, err := pgcache.ResolveMigrationCatalogPath(fsys, hash, prefix); err != nil { | ||
| return "", err | ||
| } else if ok { | ||
| @@ -381,13 +416,23 @@ func getMigrationsCatalogRef(ctx context.Context, noCache bool, fsys afero.Fs, p | ||
| if noCache { | ||
| return writeTempCatalog(fsys, noCacheMigrationsCatalogPath, snapshot) | ||
| } | ||
| if zeroMigrations { | ||
| // MigrateShadowDatabase with zero migrations == the platform baseline. | ||
| if err := ensureTempDir(fsys); err != nil { | ||
| return "", err | ||
| } | ||
| if err := utils.WriteFile(baselinePath, []byte(snapshot), fsys); err != nil { | ||
| return "", err | ||
| } | ||
| return baselinePath, nil | ||
| } | ||
| return pgcache.WriteMigrationCatalogSnapshot(fsys, prefix, hash, snapshot) | ||
| } | ||
| // getDeclarativeCatalogRef applies local declarative files to a shadow database | ||
| // and exports the resulting catalog for diffing. | ||
| func getDeclarativeCatalogRef(ctx context.Context, noCache bool, fsys afero.Fs, options ...func(*pgx.ConnConfig)) (string, error) { | ||
| hash, err := hashDeclarativeSchemas(fsys) | ||
| hash, err := declarativeCatalogCacheKey(fsys) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| @@ -439,9 +484,9 @@ func writeDeclarativeCatalogFromConfig(ctx context.Context, config pgconn.Config | ||
| return path, nil | ||
| } | ||
| // createShadow provisions and health-checks the temporary Postgres container | ||
| // used by declarative conversion and diff operations. | ||
| func createShadow(ctx context.Context) (string, pgconn.Config, error) { | ||
| // createShadowContainer provisions and health-checks the temporary Postgres | ||
| // container used by declarative conversion and diff operations. | ||
| func createShadowContainer(ctx context.Context) (string, pgconn.Config, error) { | ||
| fmt.Fprintln(os.Stderr, "Creating shadow database...") | ||
| shadow, err := diff.CreateShadowDatabase(ctx, utils.Config.Db.ShadowPort) | ||
| if err != nil { | ||
| @@ -628,6 +673,94 @@ func baselineVersionToken() string { | ||
| return catalogPrefixRegexp.ReplaceAllString(image, "-") | ||
| } | ||
| // setupInputsToken hashes every project input that start.SetupDatabase consumes | ||
| // and that therefore shapes the platform baseline: | ||
| // | ||
| // - the Postgres image (initSchema content); | ||
| // - the service toggles that gate initSchema — auth/storage/realtime; | ||
| // - api.auto_expose_new_tables (ApplyApiPrivileges default ACLs); | ||
| // - vault secret names (UpsertVaultSecrets); | ||
| // - supabase/roles.sql (SeedGlobals). | ||
| // | ||
| // Every catalog produced in this flow is "platform baseline + {nothing | migrations | ||
| // | declarative}", so each cache folds this token into its key and self-invalidates | ||
| // when setup changes instead of reusing a snapshot from a different baseline. | ||
| func setupInputsToken(fsys afero.Fs) (string, error) { | ||
| h := sha256.New() | ||
| fmt.Fprintln(h, baselineVersionToken()) | ||
| // initSchema conditionally provisions these service schemas. | ||
| fmt.Fprintf(h, "auth=%t storage=%t realtime=%t\n", | ||
| utils.Config.Auth.Enabled, utils.Config.Storage.Enabled, utils.Config.Realtime.Enabled) | ||
| // api.auto_expose_new_tables drives ApplyApiPrivileges (default ACLs). | ||
| if v := utils.Config.Api.AutoExposeNewTables; v != nil { | ||
| fmt.Fprintf(h, "auto_expose_new_tables=%t\n", *v) | ||
| } else { | ||
| fmt.Fprintln(h, "auto_expose_new_tables=unset") | ||
| } | ||
avallete marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| // Vault secrets are created during setup; key on their names. | ||
| names := make([]string, 0, len(utils.Config.Db.Vault)) | ||
| for name := range utils.Config.Db.Vault { | ||
| names = append(names, name) | ||
| } | ||
| sort.Strings(names) | ||
| for _, name := range names { | ||
| fmt.Fprintf(h, "vault=%s\n", name) | ||
| } | ||
| // supabase/roles.sql is seeded into the baseline. | ||
| roles, err := afero.ReadFile(fsys, utils.CustomRolesPath) | ||
| if err != nil && !errors.Is(err, os.ErrNotExist) { | ||
| return "", err | ||
| } | ||
| if _, err := h.Write(roles); err != nil { | ||
| return "", err | ||
| } | ||
| return hex.EncodeToString(h.Sum(nil))[:12], nil | ||
| } | ||
| // baselineCatalogKey derives the cache key for the platform baseline catalog. | ||
| // | ||
| // Keying only by image would let a stale baseline — produced by a pre-platform- | ||
| // baseline CLI, a different image, or different service/api/vault/roles config — be | ||
| // reused as the no-migration diff source, leaking spurious objects into generated | ||
| // migrations until .temp/pgdelta is cleared. The image token stays as a human- | ||
| // readable prefix; old bare-baseline files keyed by the token alone no longer | ||
| // match, so they are never reused. | ||
| func baselineCatalogKey(fsys afero.Fs) (string, error) { | ||
| token, err := setupInputsToken(fsys) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| return baselineVersionToken() + "-" + token, nil | ||
| } | ||
| // baselineCatalogPath returns the on-disk path of the platform baseline catalog | ||
| // for the current project inputs. Both the generate writer and the no-migration | ||
| // sync reader resolve the path through this helper so they always agree. | ||
| func baselineCatalogPath(fsys afero.Fs) (string, error) { | ||
| key, err := baselineCatalogKey(fsys) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| return filepath.Join(pgDeltaTempPath(), fmt.Sprintf(baselineCatalogName, key)), nil | ||
| } | ||
| // declarativeCatalogCacheKey keys the warmed declarative target catalog by both the | ||
| // declarative SQL files and the setup inputs. The target is the platform baseline | ||
| // plus the declarative schema, so a change to either must invalidate it; otherwise | ||
| // sync could pair a freshly keyed source baseline with a target warmed under a | ||
| // different setup, emitting platform/config-only differences as user migrations. | ||
| func declarativeCatalogCacheKey(fsys afero.Fs) (string, error) { | ||
| schemaHash, err := hashDeclarativeSchemas(fsys) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| setup, err := setupInputsToken(fsys) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| return setup + "-" + schemaHash, nil | ||
| } | ||
| func sanitizedCatalogPrefix(prefix string) string { | ||
| prefix = strings.TrimSpace(prefix) | ||
| if len(prefix) == 0 { | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.