Skip to content

GitOps drift detection: manual-sync gate, stale comparisons, coverage audit, ignoreDifferences disclosure#1136

Closed
nadaverell wants to merge 3 commits into
mainfrom
feature/argo-drift-detection
Closed

GitOps drift detection: manual-sync gate, stale comparisons, coverage audit, ignoreDifferences disclosure#1136
nadaverell wants to merge 3 commits into
mainfrom
feature/argo-drift-detection

Conversation

@nadaverell

@nadaverell nadaverell commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

What & why

A customer asked for "drift between deployments and gitops". Radar already reads Argo's own per-resource comparison verdicts, but four detection gaps kept real drift invisible outside the detail page. This PR closes them — all credential-free (the Argo-API deep-diff view lands separately, stacked on this branch). Plan settled via cross-model plan review; part of the #601 arc.

1. Manual-sync drift reaches the issues stream

The issues engine deliberately skipped OutOfSync for non-auto-synced apps ("too chatty"). For a manual-sync shop that's the mainline drift case — it accumulates silently, fleet-wide. Now: a manual-sync Application observed continuously OutOfSync for >24h emits a gitops_out_of_sync warning (reason OutOfSyncManual). Argo records no sync-status transition time, so drift onset is tracked Radar-side per app UID on the ResourceCache (context-switch drops it; restart resets the clock — deliberately conservative, documented). Fresh drift on a manual app stays a workflow state, not an issue.

2. New gitops_stale category: frozen sync verdicts

Argo only re-compares on refresh — a down/wedged application-controller silently freezes every verdict ("Synced" lies). Two-level detection avoids the noise storm:

  • controller visible + not ready → one critical rollup on the controller workload ("sync status and drift detection are frozen for N Applications")
  • controller healthy + majority of ≥3 apps stale → one warning rollup
  • isolated stale app → per-app warning
    Threshold: max(30m, 10× argocd-cm timeout.reconciliation). Controller pods found by label across all namespaces (not hard-coded argocd). Never-reconciled / terminating / mid-operation apps excluded.

3. Cluster Audit: GitOps coverage

gitops-unmanaged-workload + gitops-helm-only-workload (independently toggleable) flag top-level workloads outside GitOps, classified via the existing pkg/subject overlay tiers — no new label heuristics. Gated on actual GitOps root objects existing (not CRD registration — leftover CRDs after uninstall must not flag the world). Argo's label-tracking mode is recognized by cross-referencing app.kubernetes.io/instance against real Application names. OwnerRef'd workloads skipped (tracking doesn't propagate to controller children). Native Helm = softer distinct finding.

4. ignoreDifferences comparison-coverage disclosure

Detail page now discloses spec.ignoreDifferences (rule count + kinds) as a neutral note, with an explicit warning when rules use jq expressions or managed-fields managers — Radar's drift filter doesn't evaluate those, so the panel can show entries Argo's UI suppresses.

Wire/contract notes

  • gitops_stale is an additive issuesapi category (catalog + control_plane group + CEL-filterable). Consumers with exhaustive switches need awareness.
  • issuesapi severity contract unchanged (critical|warning; both new detections normalize into it).
  • GitOpsInsightSummary.ignoredDifferences is additive-optional on the k8s-ui types.

Testing

Table-driven tests across all four features (drift-gate boundaries + state reset, stale rollup rules + threshold parsing + eligibility exclusions, coverage tier classification incl. label-tracking cross-reference + gate-closed, disclosure counts). Full suite + tsc green in both modules. Cross-model review ran pre-PR; its four findings (CRD-gate false positives, label-tracking miss, purge race, managedFieldsManagers disclosure) are fixed in the second commit.

Visual test: skipped — the only UI change is a small conditional text note in an existing disclaimer slot; the phase-2 PR (diff view) will run the full gitops-demo visual pass.


Note

Medium Risk
Changes GitOps issue and audit classification logic where mis-gating or overlay tiers could add noise or miss real drift; new additive gitops_stale category affects issue consumers with exhaustive switches.

Overview
Extends GitOps drift visibility beyond the detail page: issues, audit checks, and a small UI disclosure.

Issues — manual-sync drift: Manual Argo apps that stay OutOfSync for 24h+ now emit a warning (OutOfSyncManual), with drift onset tracked per Application UID on the cluster ResourceCache (resets on sync; conservative after restart).

Issues — frozen comparisons: Adds gitops_stale detection when Argo’s application-controller isn’t re-comparing: a critical rollup if the controller is visible but not ready, a warning rollup when a majority of a ≥3-app fleet is past threshold, otherwise per-app ComparisonStale warnings. Threshold uses max(30m, 10× argocd-cm timeout.reconciliation).

Audit — GitOps coverage: New checks gitops-unmanaged-workload and gitops-helm-only-workload classify top-level workloads via subject.ResolveOverlay, only when real GitOps root objects exist (gitOpsRoots in the audit runner, not CRD-only). Argo label-tracking is recognized when app.kubernetes.io/instance matches a live Application name; owner-referenced children are skipped.

GitOps UI: Drift view shows a neutral note for spec.ignoreDifferences (counts + kinds), including when rules use jq/managed-fields features Radar doesn’t evaluate.

Also wires new reasons into issue category classification and adds table-driven tests. The diff includes large audit/plan markdown captures (AUDIT-*.md, gcp-issues.md, etc.) unrelated to runtime behavior.

Reviewed by Cursor Bugbot for commit 8dfa00f. Bugbot is set up for automated code reviews on this repo. Configure here.

… audit, ignoreDifferences disclosure

Four detection-completeness gaps for Argo CD drift, all credential-free:

- Manual-sync Applications continuously OutOfSync for >24h now reach the
  issues stream (gitops_out_of_sync, warning). Drift onset is tracked
  Radar-side per app UID on the ResourceCache (Argo records no sync-status
  transition time); a restart resets the clock — deliberately conservative.
- New gitops_stale category: a down application-controller emits one
  critical rollup on the controller workload ("sync verdicts are frozen"),
  a majority-stale fleet (≥3 apps) one warning rollup, and isolated stale
  apps per-app warnings — threshold max(30m, 10× argocd-cm
  timeout.reconciliation). Controller pods found by label across all
  namespaces, not a hard-coded "argocd".
- Cluster Audit: gitops-unmanaged-workload / gitops-helm-only-workload flag
  top-level workloads outside GitOps via the pkg/subject overlay tiers.
  Gated on a GitOps controller actually being installed; ownerRef'd
  workloads skipped (tracking doesn't propagate to controller children).
- GitOps detail page disclosures spec.ignoreDifferences as comparison
  coverage (rule/jq counts + kinds) — jq rules aren't evaluated by Radar,
  so the drift panel can show entries Argo's UI suppresses.
- Coverage gate now requires actual GitOps root objects, not CRD
  registration — leftover CRDs after an Argo/Flux uninstall no longer
  flag every workload as unmanaged.
- Argo label-tracking mode (application.resourceTrackingMethod: label)
  recognized: app.kubernetes.io/instance naming a real Application counts
  as GitOps-managed; the Application names ride CheckInput.ArgoAppNames.
- Manual-drift tracker purge moved to the cluster-wide pass only —
  namespace-scoped polls no longer drop other namespaces' drift clocks.
- ignoreDifferences disclosure counts managedFieldsManagers rules as
  unsupported alongside jq (jqRuleCount → unsupportedRuleCount).
@nadaverell nadaverell requested a review from hisco as a code owner July 9, 2026 06:47
Comment thread pkg/audit/gitops_coverage.go
Comment thread internal/audit/runner.go
argoAppNames = make(map[string]struct{}, len(apps))
for _, app := range apps {
argoAppNames[app.GetName()] = struct{}{}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

App names ignore namespace

Medium Severity

gitOpsRoots and argoAppTracksByLabel key Argo Applications by metadata.name only. Kubernetes allows the same Application name in different namespaces, so an unrelated app.kubernetes.io/instance value can match and skip the unmanaged-workload finding for workloads Argo does not manage.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2bb56bd. Configure here.

…tection

# Conflicts:
#	packages/k8s-ui/src/components/gitops/insights/GitOpsInsightViews.tsx

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.

There are 4 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8dfa00f. Configure here.

"warning", "GitOpsComparisonsStale",
fmt.Sprintf("%d of %d Applications have stale sync/drift comparisons — the application-controller may be overloaded or wedged", len(stale), len(eligible)), 0)
d.Action = "Check the application-controller for reconcile backlog or throttling; individual Applications' verdicts are older than expected."
return []Detection{d}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rollup issues reset issue age

Medium Severity

GitOpsControllerStalled and GitOpsComparisonsStale are built with zero DurationSeconds/AgeSeconds. fromProblem then sets FirstSeen to the compose time, so these rollups look newly opened on every refresh instead of reflecting how long comparisons have been frozen.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8dfa00f. Configure here.

if namespace == "" {
driftTracker(cache).purge(now, argoDriftRetain)
problems = append(problems, detectArgoStaleFromCache(cache, argoApps, now)...)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Namespace scope skips stale detection

Medium Severity

gitops_stale detection and drift-tracker purge run only when DetectGitOpsProblems is called with a cluster-wide namespace. The issues provider invokes it per selected namespace, so a typical namespace-filtered Issues view never emits controller-stale or fleet-stale rollups.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8dfa00f. Configure here.

// other namespaces' drift clocks and restart their 24h gate.
if namespace == "" {
driftTracker(cache).purge(now, argoDriftRetain)
problems = append(problems, detectArgoStaleFromCache(cache, argoApps, now)...)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scoped compose never purges drift

Low Severity

Manual-sync drift tracking is purged only on the cluster-wide DetectGitOpsProblems pass. When issues compose stays namespace-scoped, deleted Applications can leave entries in argoDrift indefinitely because purge never runs.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8dfa00f. Configure here.

@nadaverell

Copy link
Copy Markdown
Contributor Author

Folded into #1138 — the two stacked branches were squashed into a single commit on feature/argo-api-diff, now targeting main. All of this PR's commits (manual-sync gate, gitops_stale, coverage audit, ignoreDifferences disclosure) are included there.

@nadaverell nadaverell closed this Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant