Skip to content

singleArgoCD: createOrUpdateArgoCD unconditionally Update()s the singleton ArgoCD CR every reconcile, causing a self-sustaining fast reconcile loop and application-controller restarts #749

Closed

Description

@maximilianoPizarro

Summary

With global.singleArgoCD: true, internal/controller/pattern_controller.go's main Reconcile calls createOrUpdateArgoCD(...) (internal/controller/argo.go) unconditionally on every reconcile pass:

// We only update the clusterwide argo instance so we can define our own 'initcontainers' section
err = createOrUpdateArgoCD(r.dynamicClient, r.fullClient, getClusterWideArgoName(), clusterWideNS, patternsOperatorConfig)

createOrUpdateArgoCD (argo.go) always builds a fresh hardcoded desired spec via newArgoCD(...) and, when the object already exists, calls a plain client.Resource(gvr).Namespace(namespace).Update(...) on it -- with no diff check against the live object beforehand:

} else { // update it
    oldArgo, oldUnstructured, errGet := getArgoCDFunc(client, name, namespace)
    ...
    argo.SetResourceVersion(oldArgo.GetResourceVersion())
    ...
    _, err = client.Resource(gvr).Namespace(namespace).Update(context.TODO(), newArgo, metav1.UpdateOptions{})
}

Since the operator appears to also react to changes on the ArgoCD object it just wrote (directly or transitively, e.g. via the Application/Pattern watch chain), this Update -> triggers another reconcile -> Update again, in a self-sustaining loop that runs far faster than the documented ReconcileLoopRequeueTime (180s) steady-state interval.

Impact (observed live on OpenShift, patterns-operator v0.0.77, gitops-operator v1.21.0)

  • oc get argocd <vpArgoNamespace> -n <vpArgoNamespace> -o jsonpath='{.metadata.resourceVersion}' polled every ~10-15s shows the resourceVersion incrementing continuously with no other actor touching the object (confirmed via --show-managed-fields: the sole non-status managedFields entry for spec belongs to field-manager manager, operation: Update, matching controller-runtime's default field-manager name for a plain typed/dynamic client Update -- not ServerSideApply).
  • Confirmed the root cause is this operator specifically (not ACM policies, not the clustergroup chart, not any local pattern chart) by scaling patterns-operator-controller-manager to 0 replicas: the ArgoCD CR's resourceVersion immediately stopped changing and stayed stable for 90+ seconds; scaling back to 1 replica, the churn resumed within the next reconcile.
  • openshift-gitops-operator's own controller reacts to each of these Update()s by recomputing/reapplying the Deployments/StatefulSet it manages ("Updating StatefulSet ... updating volumes/container resources/container command/container env", "Updating Deployment 'vp-gitops-redis/repo-server/server/applicationset-controller' - updating volumes/...") roughly every 15-30s, which restarts the <name>-application-controller StatefulSet pod on that same cadence.
  • Practical effect: Argo CD Application sync operations that are Running when the controller pod restarts get interrupted and can remain stuck in operationState.phase: Running (observed for over an hour on one Application in our environment) instead of completing, and status.sync.status never settles to Synced for several Applications even though the git repo has no outstanding diff.
  • Separately (same root cause, different symptom): spec.rbac set by newArgoCD()'s hardcoded baseline (defaultPolicy: role:readonly, a 3-line policy granting only system:cluster-admins/cluster-admins/admin -> role:admin, scopes: [groups,email]) always wins over anything a pattern's own chart tries to set on the same CR (even via ServerSideApply=true), since this operator does a plain Update every reconcile. There is no values/Pattern-CR override hook for this baseline today (unlike clustergroup-chart's clusterGroup.argoCD.rbac, added in v0.9.50, which is a no-op for singleArgoCD since clustergroup-chart's own templates/plumbing/argocd.yaml renders nothing at all when global.singleArgoCD=true).

Suggested fix

Before calling Update() in createOrUpdateArgoCD, compute a semantic diff between oldArgo.Spec (or the relevant subset this function owns) and the freshly-built argo.Spec, and skip the API call entirely when they already match. This alone should stop the self-sustaining loop, since there would be nothing left to re-trigger the watch after the first successful convergence.

As a secondary/independent improvement, consider exposing an override hook (mirroring clusterGroup.argoCD.rbac on the non-singleArgoCD path) for at least spec.rbac on the clusterwide instance, so patterns that need a local ArgoCD user with more than read-only access (e.g. an MCP-server-style integration) aren't stuck with the hardcoded 3-line baseline policy.

Environment

  • patterns-operator.v0.0.77 (catalog channel fast, currently the latest available)
  • openshift-gitops-operator.v1.21.0
  • global.singleArgoCD: true, single-cluster hub-only reproduction (also affects hub+spoke topologies)
  • OpenShift 4.20

Activity

  1. mbaldessari commented on Jul 5, 2026

    @mbaldessari
    Contributor

    Hi and thanks for the report!

    A couple of things:

    • global.singleArgoCD is not a concept inside the patterns-operator, it only lives in the clustergroup chart. So I think that is either a misunderstanding or some AI hallucination?
    • I cannot reproduce this, I just tried on ocp 4.22 and ocp 4.20 with multicloud gitops from main and I get:
    # 4.20
    export KUBECONFIG=/home/michele/sno3-kubeconfig; for i in $(seq 1 12); do echo "$(date +%H:%M:%S) rv=$(oc get argocd vp-gitops -n vp-gitops -o jsonpath='{.metadata.resourceVersion}')"; sleep 15; done
    
    13:55:20 rv=27141
    13:55:35 rv=27141
    13:55:50 rv=27141
    13:56:05 rv=27141
    ....
    # 4.22
    export KUBECONFIG=/home/michele/sno1-kubeconfig; for i in $(seq 1 12); do echo "$(date +%H:%M:%S) rv=$(oc get argocd vp-gitops -n vp-gitops -o jsonpath='{.metadata.resourceVersion}')"; sleep 15; done
    13:54:35 rv=56928
    13:54:50 rv=56928
    13:55:05 rv=56928
    13:55:21 rv=56928
    ...

    If you tweak the RBACs externally (via server-side apply or other mechanisms), then yes, there will be two things racing against each other and the RV will constantly increase.

    Having said the above, I think we can definitely improve things here:

    1. If the gitops.additionalArgoAdmins string in the pattern-operator-configmap is not sufficient, we should also be able to expose the full argo RBAC override to the user, I'll tackle that
    2. We could add a compare function that checks for "our" fields, but they tend to be somewhat brittle, so I genuinely prefer exposing any knobs that we need and remain the sole owner of the ArgoCD object
  2. mbaldessari commented on Jul 5, 2026

    @mbaldessari
    Contributor

    #754 will expose the gitops.argoRBAC key in the configmap (the corresponding validatedpatterns/pattern-install-chart#46 will allow to set it directly in a pattern like this https://github.com/mbaldessari/multicloud-gitops/blob/fix-single-argoreconcile/values-global.yaml#L9-L17)

  3. maximilianoPizarro commented on Jul 6, 2026

    @maximilianoPizarro
    Author

    Thanks Michele for the detailed explanation and the PRs — confirmed on our end with testing:

    • global.singleArgoCD is indeed a clustergroup-chart concept, not patterns-operator
    • The fast reconcile loop we observed was caused by our own clusterGroup.argoCD.rbac overrides drifting the singleton ArgoCD CR spec on every reconcile (exactly what you described)
    • With the new gitops.argoRBAC key exposed via Add gitops.argoRBAC config key for full ArgoCD RBAC override #754 + pattern-install-chart#46, we can set RBAC policy correctly at the right layer without triggering the loop

    Appreciate the time and the fix. Closing this as resolved.

  4. mbaldessari commented on Jul 6, 2026

    @mbaldessari
    Contributor

    FWIW we just released 0.0.78 which exposes the rbac knob (https://github.com/validatedpatterns/pattern-install-chart/blob/main/values.yaml#L32 is the value for the CM) and we also made the ArgoCD obj updating less onerous. Thanks again for bringing this to our attention

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions