fix: preserve third-party annotations and labels when reconciling managed resources (#447)
Closes #446.
## Problem
Issue #446 identified four resources where `CreateOrUpdate` mutate funcs
replaced the annotation map wholesale:
```go
obj.Annotations = desired.Annotations
```
On any cluster where an external controller annotates operator-managed
resources, the operator strips those annotations on every reconcile,
causing the external controller to re-add them, triggering another
reconcile -- a tight loop. Confirmed trigger: Rancher stamps
`field.cattle.io/publicEndpoints` on Ingresses and Services.
On closer inspection the same bug affects **labels** across **all**
operator-managed resources, for the same reason: `obj.Labels =
desired.Labels` is wholesale replacement. Any external tool that labels
an operator-managed resource (monitoring agents, cost tooling, admission
webhooks) hits the same loop.
## Fix
Introduce a `mergeStringMap` helper that merges desired entries into the
existing map rather than replacing it:
```go
func mergeStringMap(current, desired map[string]string) map[string]string {
if current == nil {
current = make(map[string]string)
}
for k, v := range desired {
current[k] = v
}
return current
}
```
Apply it to every `.Labels` and `.Annotations` assignment across all
`CreateOrUpdate` mutate funcs in the controller (17 label sites, 5
annotation sites).
**Known limitation:** keys the operator previously managed but no longer
desires are not removed -- they remain on the object but the operator
ignores them. Implementing pruning without touching external keys would
require tracking operator-owned keys across reconciles. The added
complexity is not worth it; a stale label or annotation the operator no
longer cares about causes no harm.
## Changes
- `internal/controller/annotations.go` -- new `mergeStringMap` helper
with doc comment covering the known limitation
- `internal/controller/openclawinstance_controller.go` -- all `.Labels =
desired.Labels` and `.Annotations = desired.Annotations` assignments
replaced with `mergeStringMap`
- `internal/controller/annotations_test.go` -- unit tests covering nil
current, external key preservation, update, stale-key retention, and
label case N
Nick Marden committed
3711bce50b6de870d9f36c651bceacf0f044f5f9
Parent: 4cab3f2
Committed by GitHub <noreply@github.com>
on 4/12/2026, 11:37:41 AM