Fix GetDefaultProvider bypassing registered config factory#4740
Fix GetDefaultProvider bypassing registered config factory#4740
Conversation
config.NewDefaultProvider() skips the RegisterProviderFactory hook and always reads the local XDG config file. Switch to config.NewProvider(), which checks the registered factory first and falls back to the default only when no factory is registered. Without this fix, enterprise builds that register a factory to supply an EnterpriseProvider (e.g. fetching config from a remote config server) were silently ignored, causing thv registry commands to use the embedded registry instead of the configured one. Add unit tests covering the factory-respected and fall-through cases, caching semantics, and reset behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4740 +/- ##
==========================================
+ Coverage 68.78% 68.85% +0.07%
==========================================
Files 516 516
Lines 54307 54313 +6
==========================================
+ Hits 37353 37399 +46
+ Misses 14100 14054 -46
- Partials 2854 2860 +6 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Both NewRegistryRoutes and NewRegistryRoutesForServe called config.NewDefaultProvider(), which skips the RegisterProviderFactory hook and always reads the local XDG config file. Switch to config.NewProvider() so enterprise builds that register a factory (e.g. to supply a remote config server) are correctly honoured. Add unit tests covering the regression case and the no-factory fallback for both constructors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Go evaluates function arguments eagerly, so the previous implementation called config.NewProvider() (and thus any registered ProviderFactory) on every invocation of GetDefaultProvider, even after sync.Once had already fired and the resulting provider was discarded. Move the call inside the Do closure so the factory is invoked at most once. GetDefaultProvider now owns its sync.Once block directly rather than delegating to GetDefaultProviderWithConfig. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@claude can you review the comments are addressed? |
|
Claude finished @reyortiz3's task in 1m 21s —— View job Review: are the prior comments addressed?
Bug 1 — Eager factory invocation ✅ FixedCommit Bug 2 — Data race between
|
…Config
ResetDefaultProvider assigned defaultProviderOnce = sync.Once{} (a
non-atomic struct write) while GetDefaultProviderWithConfig called
defaultProviderOnce.Do(), which does an atomic load of the internal
done field. Mixed-width concurrent access = data race.
Replace the three separate package-level variables (sync.Once,
Provider, error) with a single providerState struct stored behind an
atomic.Pointer. ResetDefaultProvider now atomically swaps in a fresh
struct instead of writing to one that may be in use. Goroutines that
loaded a pointer before a reset keep a stable reference and complete
safely; goroutines that load after the swap initialise against the new
state. The mutex is no longer needed and is removed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@claude can you review if the comments are finally addressed? |
|
Claude finished @reyortiz3's task in 58s —— View job Review: Are the prior comments addressed?
Bug 1 — Eager factory invocation ✅ Fixed
Bug 2 — Data race ✅ Fixed (with a better approach than suggested)Rather than the mutex-before- type providerState struct {
once sync.Once
provider Provider
err error
}
var currentProviderState atomic.Pointer[providerState]
This eliminates the race cleanly: any goroutine that already holds a reference to the old This is a better solution than the mutex approach — it's lock-free and avoids holding a mutex across potentially slow initialization (config file I/O). Tests ✅The five tests cover the required scenarios:
The Summary: Both previously identified issues are fully addressed. The implementation is clean, correct, and the approach is sound. Ready to merge. |
These tests mutate global singletons (config.registeredFactory and currentProviderState) so they cannot run in parallel. Suppress the paralleltest linter warning with an explanatory comment on each test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
gci requires nolint directives to appear immediately before the func declaration, after the doc comment block. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
GetDefaultProvider()inpkg/registry/factory.gocalledconfig.NewDefaultProvider(), which bypasses theRegisterProviderFactoryhook entirely and always reads the local XDG config file. This means any enterprise build that registers a factory (e.g. to supply anEnterpriseProviderthat fetches config from a remote server) was silently ignored —thv registrycommands always showed the embedded registry instead of the configured one.config.NewDefaultProvider()toconfig.NewProvider(), which checks the registered factory first and falls back to the default only when no factory is registeredsync.Once), and reset behaviourType of change
Test plan
task test)The regression test
TestGetDefaultProvider_RespectsRegisteredFactoryregisters a factory pointing to a custom registry with a sentinel server name and asserts that server appears inListServers()— this test fails on the old code and passes with the fix.Does this introduce a user-facing change?
No change in open-source builds. In enterprise builds that register a
ProviderFactory,thv registry listand related commands now correctly use the factory-backed config (e.g. a remote registry URL from the enterprise config server) instead of falling back to the embedded registry.Generated with Claude Code