feat(config): multi-deployment environments + ResolveDatabaseTargets#213
feat(config): multi-deployment environments + ResolveDatabaseTargets#213Kiran01bm wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds first-class config + resolver support for “multi-deployment environments”, allowing a single (database, environment) to resolve to N Tern deployments deterministically, while keeping legacy single-deployment callers working (and explicitly erroring when they encounter N≠1).
Changes:
- Extend
EnvironmentConfigwith an additivedeployments:map and validation rules to prevent mixing local DSN, scalar routing, and map routing. - Introduce
ServerConfig.ResolveDatabaseTargets()and refactorResolveDatabaseTarget()to delegate and error when multiple deployments resolve. - Document the new config shape and add Helm values examples; add unit tests covering resolution and validation.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| pkg/api/config.go | Adds Deployments config shape, validates it, and implements ResolveDatabaseTargets + legacy delegation behavior. |
| pkg/api/config_test.go | Adds tests for multi-deployment resolution ordering and validation rejection cases. |
| docs/configuration.md | Documents the multi-deployment environment YAML shape and rules. |
| charts/schemabot/values.yaml | Adds a commented Helm values example showing multi-deployment configuration. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Add an optional `deployments:` map under `databases.<db>.environments.<env>` so one environment can fan out to N Tern deployments. The map key IS the Tern deployment name (same key space as `tern_deployments`, and the same value that will eventually land on the per-deployment apply row); each entry just carries `target:`. Mutually exclusive with the scalar `target`/`deployment` pair and with `dsn`/`dsn_from`. Resolver changes: - New `ResolveDatabaseTargets(database, env) ([]ResolvedDatabaseTarget, error)` returns one element per configured deployment, sorted by deployment key for deterministic ordering. Scalar remote and local DSN configs still return a single-element slice. - `ResolveDatabaseTarget` now delegates to `ResolveDatabaseTargets` and errors with a clear "use ResolveDatabaseTargets" message when the env resolves to more than one deployment. Existing single-deployment callers (plan/apply/progress handlers, integration tests) are unaffected. Validation additions in `ServerConfig.Validate()`: - reject mixing scalar `target`/`deployment` and the `deployments:` map - reject mixing local `dsn`/`dsn_from` and the `deployments:` map - reject an empty `deployments:` map - reject map entries with empty `target` - require each map key to exist in `tern_deployments` with an endpoint configured for the current environment (same cross-check as the scalar shape) Tests: - TestServerConfig_ResolveDatabaseTargets — local, scalar, sorted multi-deployment, unknown db, unknown env, multi-deployment via the legacy resolver, scalar via the legacy resolver. - TestServerConfig_DeploymentsMapValidation — happy path + six rejection cases. Docs: - docs/configuration.md gains a "Multi-Deployment Environment" section with a worked example and the validation rules. - charts/schemabot/values.yaml gains a commented multi-deployment example next to the existing local/gRPC examples. No production behavior changes yet — no caller of `ResolveDatabaseTargets` exists.
5f08dbc to
8ae60e6
Compare
ResolveDatabaseTargets used len(envConfig.Deployments) > 0 to decide
whether to take the map branch, so an explicitly configured but empty
'deployments: {}' fell through to scalar routing and returned the
misleading 'missing server-side target/deployment' error. Validate()
already catches this at config load, but the resolver should also
return a coherent error for callers that construct ServerConfig
directly (tests, hot-reload paths).
Switch the branch guard to '!= nil' and add explicit checks for
empty map and empty map keys, matching the rules already enforced
by Validate(). Adds TestServerConfig_ResolveDatabaseTargets_BypassValidate
covering all three cases.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 63d84fe79c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Validate() now rejects maps with >1 entries with a clear error. Single-entry maps still pass (behaviourally identical to scalar target/deployment). Docs and Helm example marked as PREVIEW. The ResolveDatabaseTargets resolver and types are unchanged, ready for the follow-up that wires plan/apply.
What ?
Adds an additive
deployments:map underdatabases.<db>.environments.<env>so one environment can fan out to N Tern deployments, plus a newResolveDatabaseTargetsresolver. No production behavior changes yet — no caller ofResolveDatabaseTargetsexists (yet).Why ?
The resolver shape is the contract between config and orchestration. Landing it before the apply write path is updated means downstream callers can be pure consumers of the new API — no half-migrated state in either direction.
Config shape
The map key IS the Tern deployment name — same key space as
tern_deployments, and the same value that will eventually land onapply_operations.deployment. Each entry just carriestarget:.Resolver API
Validation rules
ServerConfig.Validate()now also rejects:target/deploymentwith thedeployments:mapdsn/dsn_fromwith thedeployments:mapdeployments:maptargettern_deployments, or present but missing an endpoint for the current environment (same cross-check the scalar shape already does)Back-compat matrix
ResolveDatabaseTargetsResolveDatabaseTarget(legacy)target+deploymentdeployments:map (N entries)dsn/dsn_fromDeployment == Target == <db name>)Design decision worth flagging
The map key is the Tern deployment name, not a separate "logical deployment label". An earlier draft had
{ deployment: payments-a, target: payments }inside each entry, which would split deployment identity across two keys for no current gain. If we ever need a logical label decoupled from the Tern endpoint name (e.g., for PR-comment display), we can add it later as an additive field onDeploymentTarget.Cross-deployment promotion order (a
deployment_order:analogous toenvironment_order:) is deferred to a follow-up that also renames the scheduler to operator. This PR sorts by deployment key for deterministic resolution.Tests
TestServerConfig_ResolveDatabaseTargets: local DSN, scalar remote, sorted multi-deployment, unknown db, unknown env, multi-deployment via the legacy resolver, scalar via the legacy resolver.TestServerConfig_DeploymentsMapValidation(table): happy path + six rejection cases (mixed scalar+map, empty map, empty target, unknown deployment, deployment without endpoint for env, local DSN + map).Verification
go build ./...✅go vet ./...✅go test ./pkg/api/... ./pkg/state/... ./pkg/storage/... ./pkg/schema/... ./pkg/webhook/...✅Out of scope
ResolveDatabaseTargetsinto the apply / plan write paths — a follow-up will start writing oneapply_operationsrow per resolved deployment.deployment_order:) — deferred to the follow-up that renames scheduler to operator.Rollback
Pure revert. No on-disk artifact, no schema changes, no live callers of the new API.