Wire persistent DCRCredentialStore into EmbeddedAuthServer#5196
Draft
tgrunnagle wants to merge 2 commits intodcr-3b_issue_5184from
Draft
Wire persistent DCRCredentialStore into EmbeddedAuthServer#5196tgrunnagle wants to merge 2 commits intodcr-3b_issue_5184from
tgrunnagle wants to merge 2 commits intodcr-3b_issue_5184from
Conversation
4 tasks
b0bf320 to
1736a6e
Compare
Type EmbeddedAuthServer.dcrStore against storage.DCRCredentialStore and derive it from the same storage.Storage value returned by createStorage via a single type assertion, so a Redis-backed authserver reuses already-registered RFC 7591 clients across replicas and restarts instead of re-registering at every boot. Phase 2 left two parallel DCR stores: a runner-side in-memory map in dcr_store.go and the storage-level interface added in sub-issue 1. This collapses the runner-side implementation into a thin storageBackedStore adapter that delegates to storage.DCRCredentialStore, leaving exactly one persistence implementation per backend (storage.MemoryStorage and storage.RedisStorage). NewInMemoryDCRCredentialStore is preserved as a test helper that wraps storage.NewMemoryStorage so existing resolver tests compile unchanged; the standalone inMemoryDCRCredentialStore type and its map / RWMutex are deleted. buildPureOAuth2Config is unchanged — the wiring change swaps the implementation passed to the resolver, not the call shape. Add TestEmbeddedAuthServer_DCRSurvivesRestart in embeddedauthserver_test.go (next to TestNewEmbeddedAuthServer_DCRBoot) covering the durable-restart case: boot, close, rebuild against the same storage.MemoryStorage instance, assert the second resolve makes zero AS requests. The integration_test.go file under pkg/authserver would otherwise be the natural home, but it is in package authserver and importing runner from there would cycle (runner already imports authserver); the test docstring records this constraint.
Fixed issues from code review of #5185 wiring change: - HIGH: Storage backend leaked on NewEmbeddedAuthServer error paths. Split the constructor into a public NewEmbeddedAuthServer that calls createStorage and an unexported newEmbeddedAuthServerWithStorage that owns the cleanup contract via a deferred Close gated on a named return error. Verified by TestNewEmbeddedAuthServer_ClosesStorageOnError using a closeTrackingStorage wrapper. - MEDIUM: Comment claimed interface embedding that did not exist. Embed storage.DCRCredentialStore in the storage.Storage interface instead, promoting the runtime type assertion to a compile-time guarantee (the AC's explicitly preferred outcome). The dead error branch and its outdated comment are gone; mocks regenerated via task gen. - MEDIUM: Test placement deviated from AC instruction. Moved TestEmbeddedAuthServer_DCRSurvivesRestart out of the runner package and into a new pkg/authserver/integration_dcr_restart_test.go in package authserver_test, so the test lives next to the other pkg/authserver integration tests without inducing the runner -> authserver import cycle. Added a small public DCRStore() accessor on EmbeddedAuthServer mirroring existing IDPTokenStorage / UpstreamTokenRefresher accessors. - MEDIUM: Durable-restart not exercised end-to-end. Strengthened the restart test to go through NewEmbeddedAuthServer for the first boot (full constructor path with DCR), capture the storage via the new DCRStore() accessor, and assert the DCR row survives the first server's Close. The full "boot, close, boot again, observe zero /register" scenario remains a documented gap (the production Redis path requires Sentinel which miniredis does not speak); the gap and the conditions under which it can be closed are recorded in the test docstring per the review's accept-the-gap branch.
781a0b9 to
565fade
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## dcr-3b_issue_5184 #5196 +/- ##
=====================================================
+ Coverage 67.81% 67.89% +0.07%
=====================================================
Files 610 610
Lines 62379 62414 +35
=====================================================
+ Hits 42302 42374 +72
+ Misses 16902 16858 -44
- Partials 3175 3182 +7 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
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.
DRAFT - not ready for review
Summary
DCRCredentialStorethat lived only in the runner package. Restarting (or scaling out to) an authserver dropped every RFC 7591 client registration on the floor and re-registered against the upstream on every boot, which is unworkable for the Datadog-style upstream demo and for any multi-replica deployment. This PR wires the persistentDCRCredentialStoreintroduced in earlier sub-issues (in-memory + Redis backends) intoEmbeddedAuthServerso a Redis-backed authserver reuses already-registered clients across replicas and restarts.EmbeddedAuthServer.dcrStoreis now typed againststorage.DCRCredentialStoreand is derived from the samestorage.Storagevalue returned bycreateStorage, so a singlestorage_type: redisconfig toggles DCR persistence alongside the rest of authserver state. Thestorage.Storageinterface embedsstorage.DCRCredentialStore, promoting the previously-needed runtime type assertion to a compile-time guarantee.DCRCredentialStoreinpkg/authserver/runner/dcr_store.gois collapsed into a thinstorageBackedStoreadapter that delegates tostorage.DCRCredentialStoreand translatesDCRResolution<->DCRCredentialsat the boundary. There is now exactly one persistence implementation per backend.NewEmbeddedAuthServer(creates storage) and an unexportednewEmbeddedAuthServerWithStorage(owns the cleanup contract). Any error after entry closes the storage backend via a deferred cleanup gated on a named return error, so a crash-looping caller no longer leaks the Redis client connection pool /MemoryStoragecleanup goroutine on every restart.buildPureOAuth2Configremains pure (unchanged signature, noctx, no I/O);buildUpstreamConfigsis the boundary that consumes the resolver and overlays DCR-resolved credentials onto each upstream.DCRStore()accessor onEmbeddedAuthServermirroringIDPTokenStorage/UpstreamTokenRefresher, used by integration tests to verify the resolver and the authserver write through the same backend.This PR also lands the dependency stack that #5185 builds on (the persistent
DCRCredentialStoretypes + memory backend, the Redis backend, the operator CRD surface for DCR, and the runner-side DCR resolver wiring). Each layer was developed and reviewed as a separate commit on this branch; commits are sequenced so each one builds and tests cleanly.Closes #5185
Type of change
Test plan
task test)task test-e2e)task lint-fix)Notable test coverage added by this PR:
pkg/authserver/integration_dcr_restart_test.go(new) —TestEmbeddedAuthServer_DCRSurvivesRestartboots anEmbeddedAuthServeragainst a mock AS, captures the DCR store via the newDCRStore()accessor, closes the server, and asserts the persisted DCR row survives the first server'sClose. Lives inpackage authserver_testto avoid the runner -> authserver import cycle. The full "boot, close, boot again, observe zero/register" scenario across a fresh constructor is documented as a gap (the production Redis path requires Sentinel, which miniredis does not speak); test docstring records the conditions under which it can be closed.pkg/authserver/runner/embeddedauthserver_test.go—TestBuildUpstreamConfigs_DCRexercises first-call registration + cache-hit on the second call (zero additional HTTP requests) and asserts the caller'sRunConfig.Upstreamsslice is never mutated.TestNewEmbeddedAuthServer_ClosesStorageOnErroruses acloseTrackingStoragewrapper to verify the deferred-cleanup contract.pkg/authserver/storage/memory_test.go,redis_test.go,redis_integration_test.go— coverage for the persistentDCRCredentialStoreoperations on both backends, includingScopesHashcanonicalisation (sort + dedupe + newline join).API Compatibility
v1beta1API, OR theapi-break-allowedlabel is applied and the migration guidance is described above.The CRD changes are additive:
OAuth2UpstreamConfig.clientIdbecomes optional with a CEL constraint requiring exactly one ofclientIdordcrConfig, and a newdcrConfigfield is added. ExistingMCPExternalAuthConfig/VirtualMCPServerresources that setclientIdcontinue to validate unchanged.Does this introduce a user-facing change?
Yes. Operators of OAuth2 upstreams can now configure RFC 7591 Dynamic Client Registration in the operator CRD via
dcrConfig(withdiscoveryUrlorregistrationEndpoint, plus optionalinitialAccessTokenRef,softwareId,softwareStatement) instead of statically configuringclientId+clientSecret. When the authserver is configured withstorage_type: redis, DCR registrations persist across restarts and are shared across replicas; in single-replica memory mode, registrations live for the process lifetime as before.Special notes for reviewers
DCRCredentialStoretypes + memory + Redis backends), the operator CRD surface, and the Phase 2 resolver wiring. The size is above the usual 400-line / 10-file limit; each commit is self-contained and the stack reads top-to-bottom in commit order. Reviewers may prefer to walk the per-commit diffs./register" cross-constructor restart scenario is not exercised; closing it requires either miniredis-Sentinel emulation or a Docker-based Redis Sentinel cluster in the test harness. The wiring that the second boot would consume — the type ofdcrStorebeing the samestorage.DCRCredentialStorethatauthserver.Newwrites through — is verified at compile time bystorage.Storageembeddingstorage.DCRCredentialStoreand byTestEmbeddedAuthServer_DCRSurvivesRestartasserting the persistence boundary.buildPureOAuth2Configwas kept intentionally pure (noctx, no I/O) to preserve the architectural gate established in Authserver DCR integration (Phase 2, Steps 2a-2g) #4978; the wiring change swaps the implementation passed into the resolver, not the call shape.client_secret,registration_access_token,initial_access_token, refresh tokens) appear as arguments toslog.*calls; the grep assertion from Authserver DCR integration (Phase 2, Steps 2a-2g) #4978 still applies.