Describe the bug
After a hub cluster restart, ManagedClusterAddOn objects for enrolled clusters remain in Unknown or ConfigurationUnsupported state indefinitely. The only recovery is manual intervention (patch status.supportedConfigs by hand or restart the addon-manager).
Root cause
pkg/addon/controllers/cmainstallprogression/controller.go contains three bare PatchStatus calls (one per code path: manual, self-managed, and placement install strategies) with no RetryOnConflict around them.
On hub restart, the informer cache is rebuilt and a reconcile event fires for every ClusterManagementAddOn simultaneously. Multiple reconcile goroutines race to patch the same CMA's status.supportedConfigs. Because the sdk-go patcher makes a single client.Patch() call with no retry, concurrent attempts fail with 409 Conflict — the resourceVersion has already changed between the GET (lister cache) and the PATCH.
The 409 causes the controller to requeue the CMA, but by that time the other goroutines have all failed too. The field never gets written.
Downstream effect: ClusterManagementAddOn.status.supportedConfigs stays empty → any spec.configs patch on a ManagedClusterAddOn is rejected with ConfigurationUnsupported → the addontemplate controller cannot render ManifestWorks → all addons remain Dead/Unknown on every spoke.
Affected versions
- Confirmed on 1.3.1
- Likely also affects 1.2.x (same code path)
Reproduction steps
- Enroll 3+ spoke clusters to a hub (enough to generate concurrent CMA reconcile events)
- Restart the
cluster-manager deployment: kubectl rollout restart deployment/cluster-manager -n open-cluster-management
- Observe:
kubectl get managedclusteraddon -A shows many addons in Unknown / ConfigurationUnsupported
- Observe:
kubectl get clustermanagementaddon -o json | jq '.[].status.supportedConfigs' shows null on affected CMAs
- Observe cluster-manager logs for lines like:
failed to patch cma status ... the object has been modified; please apply your changes to the latest version
Proposed fix
Wrap each of the three PatchStatus call sites in retry.RetryOnConflict(retry.DefaultRetry, ...). Because PatchStatus takes both newObject and oldObject and creates a merge patch (it does not re-fetch internally), each retry closure must re-fetch a fresh object from the API server before re-applying the status mutation.
A fix with a unit test is available at: (add PR link once opened)
The unit test (TestPatchStatusRetriesOn409Conflict) injects 2 consecutive 409 Conflicts via a PrependReactor, runs sync(), and asserts the reconciler succeeds and writes status.defaultConfigReferences.
Workaround
Until this is fixed upstream, operators can add a "status healer" controller that bootstraps status.supportedConfigs from spec.supportedConfigs after detecting the empty-status condition:
kubectl patch clustermanagementaddon <name> \
--subresource=status --type=merge \
--patch '{"status":{"supportedConfigs":[{"group":"addon.open-cluster-management.io","resource":"addondeploymentconfigs"},{"group":"addon.open-cluster-management.io","resource":"addontemplates"}]}}'
(Repeat for each affected CMA.)
Describe the bug
After a hub cluster restart,
ManagedClusterAddOnobjects for enrolled clusters remain inUnknownorConfigurationUnsupportedstate indefinitely. The only recovery is manual intervention (patchstatus.supportedConfigsby hand or restart the addon-manager).Root cause
pkg/addon/controllers/cmainstallprogression/controller.gocontains three barePatchStatuscalls (one per code path: manual, self-managed, and placement install strategies) with noRetryOnConflictaround them.On hub restart, the informer cache is rebuilt and a reconcile event fires for every
ClusterManagementAddOnsimultaneously. Multiple reconcile goroutines race to patch the same CMA'sstatus.supportedConfigs. Because the sdk-go patcher makes a singleclient.Patch()call with no retry, concurrent attempts fail with409 Conflict— the resourceVersion has already changed between the GET (lister cache) and the PATCH.The 409 causes the controller to requeue the CMA, but by that time the other goroutines have all failed too. The field never gets written.
Downstream effect:
ClusterManagementAddOn.status.supportedConfigsstays empty → anyspec.configspatch on aManagedClusterAddOnis rejected withConfigurationUnsupported→ theaddontemplatecontroller cannot render ManifestWorks → all addons remainDead/Unknownon every spoke.Affected versions
Reproduction steps
cluster-managerdeployment:kubectl rollout restart deployment/cluster-manager -n open-cluster-managementkubectl get managedclusteraddon -Ashows many addons inUnknown/ConfigurationUnsupportedkubectl get clustermanagementaddon -o json | jq '.[].status.supportedConfigs'showsnullon affected CMAsfailed to patch cma status ... the object has been modified; please apply your changes to the latest versionProposed fix
Wrap each of the three
PatchStatuscall sites inretry.RetryOnConflict(retry.DefaultRetry, ...). BecausePatchStatustakes bothnewObjectandoldObjectand creates a merge patch (it does not re-fetch internally), each retry closure must re-fetch a fresh object from the API server before re-applying the status mutation.A fix with a unit test is available at: (add PR link once opened)
The unit test (
TestPatchStatusRetriesOn409Conflict) injects 2 consecutive 409 Conflicts via aPrependReactor, runssync(), and asserts the reconciler succeeds and writesstatus.defaultConfigReferences.Workaround
Until this is fixed upstream, operators can add a "status healer" controller that bootstraps
status.supportedConfigsfromspec.supportedConfigsafter detecting the empty-status condition:(Repeat for each affected CMA.)