feat(tern): add static target router#339
Draft
aparajon wants to merge 3 commits into
Draft
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a new data-plane routing seam for Tern by adding a targetresolver contract plus a tern.TargetRouter client that resolves opaque targets into connection inputs and caches per-target local clients. This is intended to let deployments start with static target configuration and later swap to dynamic/inventory-backed resolution without reimplementing lookup/caching.
Changes:
- Added
pkg/targetresolverwith aResolverinterface and a YAML-configurableStaticResolverbackend. - Added
pkg/tern/target_router.go, atern.Clientimplementation that resolves targets and routes operations through cachedLocalClients. - Added unit tests for both the target router and the static resolver.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/tern/target_router.go | Adds TargetRouter that resolves targets and routes/caches local clients, including observer handling. |
| pkg/tern/target_router_test.go | Adds unit tests validating basic routing/caching and observer attachment behavior. |
| pkg/targetresolver/resolver.go | Defines the resolver request/result contract for opaque target resolution. |
| pkg/targetresolver/static.go | Implements a static, YAML-backed resolver with secret expansion for DSNs. |
| pkg/targetresolver/static_test.go | Adds unit tests for static resolution, validation, and metadata cloning behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+38
to
+42
| mu sync.Mutex | ||
| clientsByTarget map[string]Client | ||
| activeObservers map[int64]ProgressObserver | ||
| pendingObservers map[string]ProgressObserver | ||
| } |
Comment on lines
+360
to
+369
| func (r *TargetRouter) clientForStoredApply(ctx context.Context, apply *storage.Apply) (Client, error) { | ||
| if apply == nil { | ||
| return nil, fmt.Errorf("stored apply is required for target routing") | ||
| } | ||
| target := apply.GetOptions().Target | ||
| if target == "" { | ||
| target = apply.Database | ||
| } | ||
| return r.clientOnlyForTarget(ctx, target, apply.DatabaseType, apply.Environment) | ||
| } |
Comment on lines
+193
to
+210
| func TestTargetRouterRoutesStoredApplyByTargetOption(t *testing.T) { | ||
| resolver := newStaticTargetResolver(t) | ||
| created := make(map[string]*targetRouterRecordingClient) | ||
| apply := &storage.Apply{ | ||
| ID: 42, | ||
| ApplyIdentifier: "apply-42", | ||
| Database: "orders", | ||
| DatabaseType: storage.DatabaseTypeMySQL, | ||
| Environment: "production", | ||
| } | ||
| apply.SetOptions(storage.ApplyOptions{Target: "dsid-orders-prod"}) | ||
| store := targetRouterApplyStore{ | ||
| byID: map[int64]*storage.Apply{42: apply}, | ||
| byIdentifier: map[string]*storage.Apply{"apply-42": apply}, | ||
| } | ||
| router := newTargetRouterForTest(t, resolver, store, created) | ||
| router.SetObserver(42, targetRouterNoopObserver{}) | ||
|
|
27358b5 to
0b1c8b0
Compare
Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019eb357-321a-7760-b4fe-e3c606cba251
0b1c8b0 to
6346e3c
Compare
…-free targets Move MySQL schema selection out of target resolution and into LocalClient execution. A target DSN that already names a database keeps the local-DSN behavior, where the namespace is an organizational label and Spirit connects to the DSN's database. A namespace-free target DSN is the inventory/data-plane shape: the concrete namespace is the connection schema and is injected per Spirit operation at plan, pull, apply, stop, cutover, and resume. This lets the target router cache one namespace-free LocalClient per target while each schema change connects to its own namespace, and keeps the inventory contract free of execution namespaces. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
controlSetup feeds Volume and other control operations; resolve the task's namespace credentials so namespace-free MySQL targets connect to the right schema instead of the namespace-free target DSN. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A data-plane process needs a reusable way to serve many opaque execution targets without each embedder rebuilding inventory lookup and per-target client caching.
What
tern.TargetRouter, atern.Clientthat resolves a target viapkg/inventoryand caches LocalClients keyed by target/type/environment (a data plane has one deployment and many targets).Risk
Low — additive and not wired into the default server path yet. Existing clients keep using the current routing unless a caller explicitly constructs the router.
🤖 Generated with Claude Code