hashicorp/consul · error
resource type %q already has a configured watch
Error message
resource type %q already has a configured watch
What it means
Controller.WithWatch registers a watch on a resource type, keyed by the type's Group-Version-Kind (resource.ToGVK). One controller can watch each type only once; a second WithWatch for the same GVK panics, because two dependency mappers for one type would race and one would be silently discarded.
Source
Thrown at internal/controller/controller.go:115
// WithReconciler changes the controller's reconciler.
func (ctl *Controller) WithReconciler(reconciler Reconciler) *Controller {
if reconciler == nil {
panic("reconciler must not be nil")
}
ctl.reconciler = reconciler
return ctl
}
// WithWatch enables watching of the specified resource type and mapping it to the managed type
// via the provided DependencyMapper. Extra cache indexes to calculate on the watched type
// may also be provided.
func (ctl *Controller) WithWatch(watchedType *pbresource.Type, mapper DependencyMapper, indexes ...*index.Index) *Controller {
key := resource.ToGVK(watchedType)
_, alreadyWatched := ctl.watches[key]
if alreadyWatched {
panic(fmt.Sprintf("resource type %q already has a configured watch", key))
}
w := newWatch(watchedType, mapper)
for _, idx := range indexes {
w.addIndex(idx)
}
ctl.watches[key] = w
return ctl
}
// WithQuery will add a named query to the controllers cache for usage during reconcile or in dependency mappers
func (ctl *Controller) WithQuery(queryName string, fn cache.Query) *Controller {
_, duplicate := ctl.queries[queryName]
if duplicate {
panic(fmt.Sprintf("a predefined cache query with name %q already exists", queryName))View on GitHub (pinned to 2397ff0d76)
Solutions
- Delete or merge the duplicate WithWatch so each GVK is registered once
- If two mappings are genuinely needed, combine them into a single DependencyMapper for that watch
- When building watches dynamically, keep a set of registered GVK strings and skip duplicates before calling WithWatch
Example fix
// before ctl.WithWatch(res.ServiceRouterType, mapper, idx) ctl.WithWatch(res.ServiceRouterType, otherMapper) // panic: already has a configured watch // after ctl.WithWatch(res.ServiceRouterType, combinedMapper, idx) // one watch, one mapper
Defensive patterns
Strategy: validation
Validate before calling
// track watched GVKs when building controllers dynamically
watched := map[string]bool{}
for _, w := range watchSpecs {
key := resource.ToGVK(w.Type)
if watched[key] {
continue // or return an error
}
watched[key] = true
ctl.WithWatch(w.Type, w.Mapper, w.Indexes...)
} Prevention
- Register each watched type from exactly one place; keep a single watch-spec list
- Merge multiple mappers for one type into a single DependencyMapper
- Add a setup unit test that builds the whole controller; duplicates panic in CI
When it happens
Trigger: Calling WithWatch twice with the same pbresource.Type (same Group/Kind/Version) on one Controller instance — e.g. copy-pasted registration blocks, a shared 'addCommonWatches' helper invoked twice, or merging two controllers that both watched the type.
Common situations: Refactoring controller setup into helper functions that get called from multiple paths; onboarding a watched type that another part of setup already registered.
Related errors
- a predefined cache query with name %q already exists
- all indexers must have a non-empty name
- no indexer was supplied when creating a new cache Index
- The Indexer must also implement one of the SingleIndexer or
- reconciler must not be nil
AI-assisted analysis of hashicorp/consul@2397ff0d76 (2026-08-15).
Data as JSON: /api/errors/2975ebf1f03a202c.
Report an issue: GitHub.