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

  1. Delete or merge the duplicate WithWatch so each GVK is registered once
  2. If two mappings are genuinely needed, combine them into a single DependencyMapper for that watch
  3. 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

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


AI-assisted analysis of hashicorp/consul@2397ff0d76 (2026-08-15). Data as JSON: /api/errors/2975ebf1f03a202c. Report an issue: GitHub.