From af6db862fab70ef2fc50637ef92bf27e22c85ce7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 20:05:00 +0000 Subject: [PATCH 1/3] Initial plan From 895201c92edfb8c97b97f690a8e00214f0d9a4a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 20:21:52 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=8C=B1=20Extract=20GitOps=20drift=20c?= =?UTF-8?q?ache=20helpers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: GitHub --- pkg/api/handlers/gitops/gitops_drift.go | 88 +++++++++++++++++++++++++ pkg/api/handlers/gitops/handler.go | 80 ---------------------- 2 files changed, 88 insertions(+), 80 deletions(-) create mode 100644 pkg/api/handlers/gitops/gitops_drift.go diff --git a/pkg/api/handlers/gitops/gitops_drift.go b/pkg/api/handlers/gitops/gitops_drift.go new file mode 100644 index 0000000000..fa0ae26de9 --- /dev/null +++ b/pkg/api/handlers/gitops/gitops_drift.go @@ -0,0 +1,88 @@ +package gitops + +import ( +"fmt" +"time" + +"github.com/gofiber/fiber/v2" +) + +// driftCacheTTL bounds how long a DetectDrift result stays in the shared +// cache feeding ListDrifts. Long enough to be useful across a dashboard +// render cycle, short enough that manual refreshes (#5952) actually see +// fresh data rather than a stale repeat. +const driftCacheTTL = 30 * time.Second + +// driftCacheEntry is a single cached drift-detection result keyed by +// repo/path/cluster/namespace. +type driftCacheEntry struct { +drifts []GitOpsDrift +detected time.Time +} + +// ListDrifts returns a list of detected drifts (for GET endpoint). +// +// #5950 — Previously this always returned an empty slice, so the UI drift +// card never showed anything. We now expose drift results cached from recent +// DetectDrift calls (see rememberDrift) filtered by the optional query +// params. Entries older than driftCacheTTL are evicted on read. +func (h *GitOpsHandlers) ListDrifts(c *fiber.Ctx) error { +cluster := c.Query("cluster") +namespace := c.Query("namespace") + +drifts := h.snapshotDrifts(cluster, namespace) +return c.JSON(fiber.Map{ +"drifts": drifts, +}) +} + +// rememberDrift stores a drift-detection result in the in-memory cache keyed +// by repo URL / path / cluster / namespace. Safe for concurrent use. +func (h *GitOpsHandlers) rememberDrift(req DetectDriftRequest, result *DetectDriftResponse) { +if result == nil { +return +} +key := fmt.Sprintf("%s|%s|%s|%s", req.RepoURL, req.Path, req.Cluster, req.Namespace) +drifts := make([]GitOpsDrift, 0, len(result.Resources)) +if result.Drifted { +for _, r := range result.Resources { +drifts = append(drifts, GitOpsDrift{ +Resource: r.Name, +Namespace: r.Namespace, +Cluster: req.Cluster, +Kind: r.Kind, +DriftType: "modified", +Details: fmt.Sprintf("%s: %s", r.Field, r.DiffOutput), +Severity: "medium", +}) +} +} +h.driftCacheMu.Lock() +defer h.driftCacheMu.Unlock() +h.driftCache[key] = driftCacheEntry{drifts: drifts, detected: time.Now()} +} + +// snapshotDrifts returns all cached drifts matching the optional +// cluster/namespace filter, dropping entries older than driftCacheTTL. +func (h *GitOpsHandlers) snapshotDrifts(cluster, namespace string) []GitOpsDrift { +now := time.Now() +h.driftCacheMu.Lock() +defer h.driftCacheMu.Unlock() +out := make([]GitOpsDrift, 0) +for k, entry := range h.driftCache { +if now.Sub(entry.detected) > driftCacheTTL { +delete(h.driftCache, k) +continue +} +for _, d := range entry.drifts { +if cluster != "" && d.Cluster != cluster { +continue +} +if namespace != "" && d.Namespace != namespace { +continue +} +out = append(out, d) +} +} +return out +} diff --git a/pkg/api/handlers/gitops/handler.go b/pkg/api/handlers/gitops/handler.go index 45990dc957..f43c7f458b 100644 --- a/pkg/api/handlers/gitops/handler.go +++ b/pkg/api/handlers/gitops/handler.go @@ -59,19 +59,6 @@ type GitOpsDrift struct { Severity string `json:"severity"` // low, medium, high } -// driftCacheTTL bounds how long a DetectDrift result stays in the shared -// cache feeding ListDrifts. Long enough to be useful across a dashboard -// render cycle, short enough that manual refreshes (#5952) actually see -// fresh data rather than a stale repeat. -const driftCacheTTL = 30 * time.Second - -// driftCacheEntry is a single cached drift-detection result keyed by -// repo/path/cluster/namespace. -type driftCacheEntry struct { - drifts []GitOpsDrift - detected time.Time -} - // GitOpsHandlers handles GitOps-related API endpoints type GitOpsHandlers struct { bridge *mcppkg.Bridge @@ -109,57 +96,6 @@ func NewGitOpsHandlers(bridge *mcppkg.Bridge, k8sClient *k8s.MultiClusterClient, // admin-only helper was removed in #6022 when the policy was loosened to // editor-or-admin for mutations and viewer-or-above for drift detection. -// rememberDrift stores a drift-detection result in the in-memory cache keyed -// by repo URL / path / cluster / namespace. Safe for concurrent use. -func (h *GitOpsHandlers) rememberDrift(req DetectDriftRequest, result *DetectDriftResponse) { - if result == nil { - return - } - key := fmt.Sprintf("%s|%s|%s|%s", req.RepoURL, req.Path, req.Cluster, req.Namespace) - drifts := make([]GitOpsDrift, 0, len(result.Resources)) - if result.Drifted { - for _, r := range result.Resources { - drifts = append(drifts, GitOpsDrift{ - Resource: r.Name, - Namespace: r.Namespace, - Cluster: req.Cluster, - Kind: r.Kind, - DriftType: "modified", - Details: fmt.Sprintf("%s: %s", r.Field, r.DiffOutput), - Severity: "medium", - }) - } - } - h.driftCacheMu.Lock() - defer h.driftCacheMu.Unlock() - h.driftCache[key] = driftCacheEntry{drifts: drifts, detected: time.Now()} -} - -// snapshotDrifts returns all cached drifts matching the optional -// cluster/namespace filter, dropping entries older than driftCacheTTL. -func (h *GitOpsHandlers) snapshotDrifts(cluster, namespace string) []GitOpsDrift { - now := time.Now() - h.driftCacheMu.Lock() - defer h.driftCacheMu.Unlock() - out := make([]GitOpsDrift, 0) - for k, entry := range h.driftCache { - if now.Sub(entry.detected) > driftCacheTTL { - delete(h.driftCache, k) - continue - } - for _, d := range entry.drifts { - if cluster != "" && d.Cluster != cluster { - continue - } - if namespace != "" && d.Namespace != namespace { - continue - } - out = append(out, d) - } - } - return out -} - // DriftedResource represents a resource that has drifted from git type DriftedResource struct { Kind string `json:"kind"` @@ -256,22 +192,6 @@ type Operator struct { Cluster string `json:"cluster,omitempty"` } -// ListDrifts returns a list of detected drifts (for GET endpoint). -// -// #5950 — Previously this always returned an empty slice, so the UI drift -// card never showed anything. We now expose drift results cached from recent -// DetectDrift calls (see rememberDrift) filtered by the optional query -// params. Entries older than driftCacheTTL are evicted on read. -func (h *GitOpsHandlers) ListDrifts(c *fiber.Ctx) error { - cluster := c.Query("cluster") - namespace := c.Query("namespace") - - drifts := h.snapshotDrifts(cluster, namespace) - return c.JSON(fiber.Map{ - "drifts": drifts, - }) -} - // ListHelmReleases returns all Helm releases across all namespaces func (h *GitOpsHandlers) ListHelmReleases(c *fiber.Ctx) error { cluster := c.Query("cluster") From f4bb66e17e824aef373b3d290a30a4afa68badc4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 20:26:25 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=8C=B1=20Format=20GitOps=20drift=20fi?= =?UTF-8?q?le=20with=20gofmt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: GitHub --- pkg/api/handlers/gitops/gitops_drift.go | 104 ++++++++++++------------ 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/pkg/api/handlers/gitops/gitops_drift.go b/pkg/api/handlers/gitops/gitops_drift.go index fa0ae26de9..0c633c7c5b 100644 --- a/pkg/api/handlers/gitops/gitops_drift.go +++ b/pkg/api/handlers/gitops/gitops_drift.go @@ -1,10 +1,10 @@ package gitops import ( -"fmt" -"time" + "fmt" + "time" -"github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2" ) // driftCacheTTL bounds how long a DetectDrift result stays in the shared @@ -16,8 +16,8 @@ const driftCacheTTL = 30 * time.Second // driftCacheEntry is a single cached drift-detection result keyed by // repo/path/cluster/namespace. type driftCacheEntry struct { -drifts []GitOpsDrift -detected time.Time + drifts []GitOpsDrift + detected time.Time } // ListDrifts returns a list of detected drifts (for GET endpoint). @@ -27,62 +27,62 @@ detected time.Time // DetectDrift calls (see rememberDrift) filtered by the optional query // params. Entries older than driftCacheTTL are evicted on read. func (h *GitOpsHandlers) ListDrifts(c *fiber.Ctx) error { -cluster := c.Query("cluster") -namespace := c.Query("namespace") + cluster := c.Query("cluster") + namespace := c.Query("namespace") -drifts := h.snapshotDrifts(cluster, namespace) -return c.JSON(fiber.Map{ -"drifts": drifts, -}) + drifts := h.snapshotDrifts(cluster, namespace) + return c.JSON(fiber.Map{ + "drifts": drifts, + }) } // rememberDrift stores a drift-detection result in the in-memory cache keyed // by repo URL / path / cluster / namespace. Safe for concurrent use. func (h *GitOpsHandlers) rememberDrift(req DetectDriftRequest, result *DetectDriftResponse) { -if result == nil { -return -} -key := fmt.Sprintf("%s|%s|%s|%s", req.RepoURL, req.Path, req.Cluster, req.Namespace) -drifts := make([]GitOpsDrift, 0, len(result.Resources)) -if result.Drifted { -for _, r := range result.Resources { -drifts = append(drifts, GitOpsDrift{ -Resource: r.Name, -Namespace: r.Namespace, -Cluster: req.Cluster, -Kind: r.Kind, -DriftType: "modified", -Details: fmt.Sprintf("%s: %s", r.Field, r.DiffOutput), -Severity: "medium", -}) -} -} -h.driftCacheMu.Lock() -defer h.driftCacheMu.Unlock() -h.driftCache[key] = driftCacheEntry{drifts: drifts, detected: time.Now()} + if result == nil { + return + } + key := fmt.Sprintf("%s|%s|%s|%s", req.RepoURL, req.Path, req.Cluster, req.Namespace) + drifts := make([]GitOpsDrift, 0, len(result.Resources)) + if result.Drifted { + for _, r := range result.Resources { + drifts = append(drifts, GitOpsDrift{ + Resource: r.Name, + Namespace: r.Namespace, + Cluster: req.Cluster, + Kind: r.Kind, + DriftType: "modified", + Details: fmt.Sprintf("%s: %s", r.Field, r.DiffOutput), + Severity: "medium", + }) + } + } + h.driftCacheMu.Lock() + defer h.driftCacheMu.Unlock() + h.driftCache[key] = driftCacheEntry{drifts: drifts, detected: time.Now()} } // snapshotDrifts returns all cached drifts matching the optional // cluster/namespace filter, dropping entries older than driftCacheTTL. func (h *GitOpsHandlers) snapshotDrifts(cluster, namespace string) []GitOpsDrift { -now := time.Now() -h.driftCacheMu.Lock() -defer h.driftCacheMu.Unlock() -out := make([]GitOpsDrift, 0) -for k, entry := range h.driftCache { -if now.Sub(entry.detected) > driftCacheTTL { -delete(h.driftCache, k) -continue -} -for _, d := range entry.drifts { -if cluster != "" && d.Cluster != cluster { -continue -} -if namespace != "" && d.Namespace != namespace { -continue -} -out = append(out, d) -} -} -return out + now := time.Now() + h.driftCacheMu.Lock() + defer h.driftCacheMu.Unlock() + out := make([]GitOpsDrift, 0) + for k, entry := range h.driftCache { + if now.Sub(entry.detected) > driftCacheTTL { + delete(h.driftCache, k) + continue + } + for _, d := range entry.drifts { + if cluster != "" && d.Cluster != cluster { + continue + } + if namespace != "" && d.Namespace != namespace { + continue + } + out = append(out, d) + } + } + return out }