Skip to content

feat(databases): apply migrations through migration runs under a grant - #478

Closed
tkkhq wants to merge 21 commits into
feat/grant-invokefrom
feat/grant-migration-runs
Closed

tkkhq wants to merge 21 commits into
feat/grant-invokefrom
feat/grant-migration-runs

Conversation

@tkkhq

@tkkhq tkkhq commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Closed: superseded by #365, which shipped databases migration apply, status, baseline and runs, and moved deploy --with-migrations to tracked runs for every credential. They work under a delegated grant (GET /capabilities is public; the run and baseline routes are on Hosting's grant route table), so the Builder worker calls apply instead of up. #479 now stacks on #477 and carries a test that apply under a grant uses only those routes.

Tracking

Why

  • volcano cloud databases migration up reads the database's connection_string (GET /projects/{id}/databases/{name}) and connects to Postgres from the client. A grant can't do that: the connection string is a credential that outlives the run, and Hosting keeps that route off the grant table. In local mode the direct path also used a fixed port and could write into another stack's database.
  • Hosting already runs migrations and baselines server-side, behind the database_migrations gate, and admits a grant on those routes.

Summary

Kept to what Hosting requires ("the CLI carries minimal changes; things are controlled server-side"). Hosting decides, the CLI sends and reports.

  • add internal/api/database_migration_runs.go: CreateDatabaseMigrationRun (sends an Idempotency-Key; a 200 replay counts like a 202), GetDatabaseMigrationRun, and BaselineDatabaseMigrations. A 409 keeps the API's code and names the run in progress
  • add internal/cmd/databases/migration/runs.go. runUp branches to it under a pg- grant before it would read the database, so up --all, up -f <file> and deploy --with-migrations (through ApplyAll) all use a run. Other credentials keep today's direct path
  • the run path sends the selected top-level files and prints the run ID. It polls every 2 s, printing each phase, then each file's outcome, the warnings and the backup name
  • a failed run's error carries each failed file's own error from the rehearsal or the database, which names the object (for example 002_second.sql: relation "second" already exists (line 1, SQLSTATE 42P07)), so the caller can decide what to baseline. That part is marked sensitive: it can quote SQL and data, so usage telemetry replaces it with [redacted]
  • add databases migration baseline -d <db> (--all | -f <file>...) [--yes] [--json], which records the named files through POST …/databases/{name}/migrations/baseline and never reads the connection string. Baselining is explicit: the caller names the files, and the CLI detects nothing. Flags match the baseline command in feat(databases): apply migrations in tracked runs #365, so the two merge. It prompts unless --yes, and with VOLCANO_NONINTERACTIVE it refuses without --yes, before any request
  • left to Hosting: a run already in progress (409 migration_run_in_progress names it; the caller runs again later), file-name rules (400), the gate being off (404 feature_unavailable; the CLI never falls back to the direct path), and every grant refusal and budget
  • a 401 or 403 while polling stops with "the run carries on on the server"; up to 5 network failures in a row are retried; the command stops following after 60 minutes
  • scope (decided by Ted): this PR moves only grants to migration runs. Every other credential moves through feat(databases): apply migrations in tracked runs #365 (databases migration apply) once database_migrations is on in production. Whichever of feat(databases): apply migrations in tracked runs #365 and this PR lands second takes in the other; the overlap is one branch in runUp, the baseline command, and the new files
  • auto-detecting a database migrated earlier with up was dropped (Ted). Nothing a grant can call says which files a database already holds, and a whole-database guess would also baseline a new file. The caller reads the failed run's error and baselines the files it names
  • each run start or baseline spends one of the grant's 100 mutations; polls are reads
  • docs: a paragraph in docs/databases.md, and one in up --help
  • removed in review as not required by Hosting: waiting on a run in progress and retrying, a client-side .sql name check, and a SQLSTATE-based baseline hint

Verification

  • go build ./...
  • make check (openapi-generated-check, release-workflow-check, lint with 0 issues, test)
  • internal/cmd/databases/migration/runs_test.go runs up with the grant against a recording migration-runs API and an executor that fails if called. It covers the happy path (POST then reads, request body, idempotency key), -f sending one file, a 200 replay that already ended, a failed run whose error names the object and is marked sensitive, feature_unavailable, a 409 naming the run in progress, a 401 while polling, a spent budget keeping exit 6, and ApplyAll. Each asserts there is no GET …/databases/app and that the grant is never printed
  • internal/cmd/databases/migration/baseline_test.go: named files and --all go to the baseline route with their SQL; --json; refused without --yes when no one can answer; checksum_mismatch, grant_budget_exhausted, grant_route_not_allowed and feature_unavailable keep their status and code; an unknown file is refused before any request
  • TestGrantMigrationUpUsesAMigrationRun and TestGrantBaselineUsesTheBaselineRoute in cmd/volcano run through the real command tree with the worker's environment (baseline: exit 0, 5 when refused, 6 on a spent budget)
  • internal/cmd/deploy/grant_test.go runs the whole-project deploy under a grant against a copy of Hosting's route table (DefaultGrantRoutes() at 2f42b4c314 on tkkhq/grant-acceptance-invoke, Kong/volcano-hosting#3329, the top of the grant chain), with each route's kind and the config dry-run-as-read rule. Every request, the source-mode read included, is on the table and carries the grant. --with-migrations uses a run and never reads the database, and a 429 grant_budget_exhausted on each mutation keeps exit 6
  • source-export (done): volcano cloud deploy first reads GET /projects/{id}/source-export. Hosting C2 lists it as project.read, with no CLI change. The copied table in internal/cmd/deploy/grant_test.go now has it, and TestDeployUnderAGrantIsRefusedTheSourceModeRead is removed
  • config dry run (decided): PUT …/config?dry_run=true counts as a read in Hosting C3, with no CLI change
  • not run: make localmode-e2e and the cloud E2E suites (Docker and staging are out of scope here)

Decisions for Ted

@tkkhq
tkkhq marked this pull request as ready for review November 13, 2026 12:23
@tkkhq
tkkhq requested a review from a team as a code owner November 13, 2026 12:23
Copilot AI balanced review requested due to automatic review settings November 13, 2026 12:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved findings include potential SQL or private-data leakage into telemetry and misleading retry guidance that can lead callers to start another mutation rather than resume polling.

4 open issues
What changed in this PR

This PR enables database migrations under delegated grants through server-side migration runs, extending the grant support added by the preceding authentication and invocation changes. Other credentials retain the existing direct migration path.

Changes:

  • Route grant-based migration up and deploy --with-migrations through tracked runs, with polling, outcome reporting, and sensitive-error handling.
  • Add an explicit migration baseline command with confirmation and JSON output.
  • Add API integration and command-tree tests, plus documentation for grant-based migrations.
File Description
internal/​cmd/​deploy/​grant_test.go Tests deploy routes, migration runs, and grant-budget refusals.
internal/​cmd/​databases/​migration/​runs.go Starts, follows, and reports server-side migration runs.
internal/​cmd/​databases/​migration/​runs_test.go Tests grant migration requests, outcomes, and failures.
internal/​cmd/​databases/​migration/​migration.go Registers baseline and routes grant migrations to runs.
internal/​cmd/​databases/​migration/​baseline.go Implements file selection, confirmation, and baseline reporting.
internal/​cmd/​databases/​migration/​baseline_test.go Tests baseline selection, output, and refusals.
internal/​api/​database_migration_runs.go Adds run creation, polling, baseline, and conflict handling.
docs/​databases.md Explains server-side migrations under delegated grants.
cmd/​volcano/​grant_test.go Tests migration commands through the real command tree.

Review effort: Balanced

Comment thread internal/api/database_migration_runs.go Resolved
Comment thread docs/databases.md Resolved
Comment thread docs/databases.md Resolved
swkeever added a commit that referenced this pull request Nov 13, 2026
Rename internal/api/database_migrations.go to database_migration_runs.go
and the `migration runs` commands from runs.go to runs_list.go and
runs_get.go, and name the in-progress code MigrationRunInProgressCode,
so #478 can rebase onto this branch without both PRs adding runs.go.

Run reads and a refused run start keep only an error body's own fields,
since a run's file errors can quote data.
swkeever added a commit that referenced this pull request Nov 13, 2026
* feat(databases): apply migrations in tracked runs

Add `databases migration status`, `apply`, `baseline`, and `runs list|get`
over the hosting migration-history and migration-run API. `deploy
--with-migrations` now starts a tracked run per database instead of running
every file through the database proxy, and reads `volcano/migrations` with
the Git deploy layout: files at the top level for one database, or one
directory per database.

Tracked runs need the `databases.migrations` capability. Where the API does
not offer it, apply, baseline, and --with-migrations refuse before anything
changes and name the untracked `databases migration up --all`, which stays.

* fix(databases): address review of tracked migration runs

- Refuse a migration whose name doesn't end in lowercase .sql before a
  tracked run or baseline sends it, naming the file to rename. Untracked
  `up` still runs such files.
- Send the baseline prompt to stderr with --json, so stdout stays JSON.
- Stop claiming a backup was taken when the run ended in its backup phase.
- Name the database's directory when a per-database deploy finds it empty,
  and in `up`'s "not found" error.

* refactor(databases): align migration run files with the grant path

Rename internal/api/database_migrations.go to database_migration_runs.go
and the `migration runs` commands from runs.go to runs_list.go and
runs_get.go, and name the in-progress code MigrationRunInProgressCode,
so #478 can rebase onto this branch without both PRs adding runs.go.

Run reads and a refused run start keep only an error body's own fields,
since a run's file errors can quote data.

* docs(databases): document the no-transaction migration marker

Tracked runs accept a file whose first line is `-- volcano:no-transaction`, which runs alone outside a transaction and again from its start on a retry. docs/databases.md and `migration apply --help` said such statements could not run at all.
@swkeever

Copy link
Copy Markdown
Collaborator

Overlap note: #365 merged (b46711d), so this PR rebases onto main and takes in #365.

What main now provides:

  • internal/api/database_migration_runs.go: the same file name as here, with this PR's names and bodies for CreateDatabaseMigrationRun, BaselineDatabaseMigrations, GetDatabaseMigrationRun, migrationConflictError, MigrationRunInProgressCode and MigrationRunInProgressError (its Error() doesn't append the run ID). It also adds GetDatabaseMigrations and ListDatabaseMigrationRuns, and run reads keep only error fields (errorFieldsOnly).
  • internal/cmd/databases/migration/runs_list.go and runs_get.go for migration runs list|get. There's no runs.go on main, so this PR's runs.go adds without conflict, and none of its identifiers collide with the package.

What this PR still has to fold in:

  • baseline.go: both PRs add it, with the same flags. Keep one command, plus this PR's VOLCANO_NONINTERACTIVE refusal if it isn't covered.
  • its grant branch in runUp (migration.go, which feat(databases): apply migrations in tracked runs #365 also changed).
  • FeatureUnavailableCode, which main doesn't define.

@tkkhq tkkhq closed this Nov 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants